diff --git a/Glamourer/Api/GlamourerIpc.Apply.cs b/Glamourer/Api/GlamourerIpc.Apply.cs index 47c655e..d155cf2 100644 --- a/Glamourer/Api/GlamourerIpc.Apply.cs +++ b/Glamourer/Api/GlamourerIpc.Apply.cs @@ -1,8 +1,8 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.Interop.Structs; +using Glamourer.State; using Penumbra.Api.Helpers; using Penumbra.GameData.Actors; @@ -130,12 +130,12 @@ public partial class GlamourerIpc if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode)) { - _stateManager.ApplyDesign(design, state, StateChanged.Source.Ipc, lockCode); + _stateManager.ApplyDesign(state, design, new ApplySettings(Source:StateSource.Ipc, Key:lockCode)); state.Lock(lockCode); } } } private void ApplyDesignByGuid(Guid identifier, IEnumerable actors, uint lockCode) - => ApplyDesign(_designManager.Designs.FirstOrDefault(x => x.Identifier == identifier), actors, DesignConverter.Version, lockCode); + => ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode); } diff --git a/Glamourer/Api/GlamourerIpc.Events.cs b/Glamourer/Api/GlamourerIpc.Events.cs index 44f0775..8caa495 100644 --- a/Glamourer/Api/GlamourerIpc.Events.cs +++ b/Glamourer/Api/GlamourerIpc.Events.cs @@ -15,7 +15,7 @@ public partial class GlamourerIpc private readonly EventProvider> _stateChangedProvider; private readonly EventProvider _gPoseChangedProvider; - private void OnStateChanged(StateChanged.Type type, StateChanged.Source source, ActorState state, ActorData actors, object? data = null) + private void OnStateChanged(StateChanged.Type type, StateSource source, ActorState state, ActorData actors, object? data = null) { foreach (var actor in actors.Objects) _stateChangedProvider.Invoke(type, actor.Address, new Lazy(() => _designConverter.ShareBase64(state))); diff --git a/Glamourer/Api/GlamourerIpc.Revert.cs b/Glamourer/Api/GlamourerIpc.Revert.cs index 8bf47cc..44a03aa 100644 --- a/Glamourer/Api/GlamourerIpc.Revert.cs +++ b/Glamourer/Api/GlamourerIpc.Revert.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Glamourer.Events; +using Glamourer.State; using Penumbra.Api.Helpers; using Penumbra.GameData.Actors; @@ -82,7 +83,7 @@ public partial class GlamourerIpc foreach (var id in actors) { if (_stateManager.TryGetValue(id, out var state)) - _stateManager.ResetState(state, StateChanged.Source.Ipc, lockCode); + _stateManager.ResetState(state, StateSource.Ipc, lockCode); } } diff --git a/Glamourer/Api/GlamourerIpc.Set.cs b/Glamourer/Api/GlamourerIpc.Set.cs index 1a39bea..db5941f 100644 --- a/Glamourer/Api/GlamourerIpc.Set.cs +++ b/Glamourer/Api/GlamourerIpc.Set.cs @@ -1,7 +1,9 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; +using Glamourer.Designs; using Glamourer.Events; using Glamourer.Services; +using Glamourer.State; using Penumbra.Api.Helpers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -55,7 +57,7 @@ public partial class GlamourerIpc if (!state.ModelData.IsHuman) return GlamourerErrorCode.ActorNotHuman; - _stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key); + _stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key:key)); return GlamourerErrorCode.Success; } @@ -82,7 +84,7 @@ public partial class GlamourerIpc if (!state.ModelData.IsHuman) return GlamourerErrorCode.ActorNotHuman; - _stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key); + _stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key: key)); found = true; } diff --git a/Glamourer/Automation/ApplicationType.cs b/Glamourer/Automation/ApplicationType.cs new file mode 100644 index 0000000..03e3a2d --- /dev/null +++ b/Glamourer/Automation/ApplicationType.cs @@ -0,0 +1,68 @@ +using Glamourer.Designs; +using Glamourer.GameData; +using Penumbra.GameData.Enums; + +namespace Glamourer.Automation; + +[Flags] +public enum ApplicationType : byte +{ + Armor = 0x01, + Customizations = 0x02, + Weapons = 0x04, + GearCustomization = 0x08, + Accessories = 0x10, + + All = Armor | Accessories | Customizations | Weapons | GearCustomization, +} + +public static class ApplicationTypeExtensions +{ + public static readonly IReadOnlyList<(ApplicationType, string)> Types = new[] + { + (ApplicationType.Customizations, + "Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."), + (ApplicationType.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."), + (ApplicationType.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."), + (ApplicationType.GearCustomization, "Apply all dye and crest changes that are enabled in this design."), + (ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), + }; + + public static (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat( + this ApplicationType type, DesignBase? design) + { + var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0) + | (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0) + | (type.HasFlag(ApplicationType.Accessories) ? AccessoryFlags : 0) + | (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0); + var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0; + var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0; + var crestFlag = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0; + var metaFlag = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0) + | (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0) + | (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0); + + if (design == null) + return (equipFlags, customizeFlags, crestFlag, parameterFlags, metaFlag); + + return (equipFlags & design!.ApplyEquip, customizeFlags & design.ApplyCustomize, crestFlag & design.ApplyCrest, + parameterFlags & design.ApplyParameters, metaFlag & design.ApplyMeta); + } + + public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; + public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet; + public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger; + + public const EquipFlag StainFlags = EquipFlag.MainhandStain + | EquipFlag.OffhandStain + | EquipFlag.HeadStain + | EquipFlag.BodyStain + | EquipFlag.HandsStain + | EquipFlag.LegsStain + | EquipFlag.FeetStain + | EquipFlag.EarsStain + | EquipFlag.NeckStain + | EquipFlag.WristStain + | EquipFlag.RFingerStain + | EquipFlag.LFingerStain; +} diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 7ab3021..50704f4 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -12,22 +12,10 @@ public class AutoDesign { public const string RevertName = "Revert"; - [Flags] - public enum Type : byte - { - Armor = 0x01, - Customizations = 0x02, - Weapons = 0x04, - GearCustomization = 0x08, - Accessories = 0x10, - - All = Armor | Accessories | Customizations | Weapons | GearCustomization, - } - - public Design? Design; - public JobGroup Jobs; - public Type ApplicationType; - public short GearsetIndex = -1; + public Design? Design; + public JobGroup Jobs; + public ApplicationType Type; + public short GearsetIndex = -1; public string Name(bool incognito) => Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text; @@ -41,10 +29,10 @@ public class AutoDesign public AutoDesign Clone() => new() { - Design = Design, - ApplicationType = ApplicationType, - Jobs = Jobs, - GearsetIndex = GearsetIndex, + Design = Design, + Type = Type, + Jobs = Jobs, + GearsetIndex = GearsetIndex, }; public unsafe bool IsActive(Actor actor) @@ -64,9 +52,9 @@ public class AutoDesign public JObject Serialize() => new() { - ["Design"] = Design?.Identifier.ToString(), - ["ApplicationType"] = (uint)ApplicationType, - ["Conditions"] = CreateConditionObject(), + ["Design"] = Design?.Identifier.ToString(), + ["Type"] = (uint)Type, + ["Conditions"] = CreateConditionObject(), }; private JObject CreateConditionObject() @@ -80,44 +68,6 @@ public class AutoDesign return ret; } - public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, bool ApplyHat, bool ApplyVisor, bool - ApplyWeapon, bool ApplyWet) ApplyWhat() - { - var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0) - | (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0) - | (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0) - | (ApplicationType.HasFlag(Type.GearCustomization) ? StainFlags : 0); - var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0; - var parameterFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeParameterExtensions.All : 0; - var crestFlag = ApplicationType.HasFlag(Type.GearCustomization) ? CrestExtensions.AllRelevant : 0; - - if (Revert) - return (equipFlags, customizeFlags, crestFlag, parameterFlags, ApplicationType.HasFlag(Type.Armor), - ApplicationType.HasFlag(Type.Armor), - ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations)); - - return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize, crestFlag & Design.ApplyCrest, - parameterFlags & Design.ApplyParameters, - ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(), - ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(), - ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(), - ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness()); - } - - public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; - public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet; - public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger; - - public const EquipFlag StainFlags = EquipFlag.MainhandStain - | EquipFlag.OffhandStain - | EquipFlag.HeadStain - | EquipFlag.BodyStain - | EquipFlag.HandsStain - | EquipFlag.LegsStain - | EquipFlag.FeetStain - | EquipFlag.EarsStain - | EquipFlag.NeckStain - | EquipFlag.WristStain - | EquipFlag.RFingerStain - | EquipFlag.LFingerStain; + public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat() + => Type.ApplyWhat(Design); } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 07bf41f..55359ca 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -1,13 +1,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Designs; +using Glamourer.Designs.Links; using Glamourer.Events; -using Glamourer.GameData; using Glamourer.Interop; using Glamourer.Interop.Structs; -using Glamourer.Services; using Glamourer.State; -using Glamourer.Unlocks; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; @@ -15,54 +13,41 @@ using Penumbra.GameData.Structs; namespace Glamourer.Automation; -public class AutoDesignApplier : IDisposable +public sealed class AutoDesignApplier : IDisposable { - private readonly Configuration _config; - private readonly AutoDesignManager _manager; - private readonly StateManager _state; - private readonly JobService _jobs; - private readonly EquippedGearset _equippedGearset; - private readonly ActorManager _actors; - private readonly CustomizeService _customizations; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly ItemUnlockManager _itemUnlocks; - private readonly AutomationChanged _event; - private readonly ObjectManager _objects; - private readonly WeaponLoading _weapons; - private readonly HumanModelList _humans; - private readonly IClientState _clientState; + private readonly Configuration _config; + private readonly AutoDesignManager _manager; + private readonly StateManager _state; + private readonly JobService _jobs; + private readonly EquippedGearset _equippedGearset; + private readonly ActorManager _actors; + private readonly AutomationChanged _event; + private readonly ObjectManager _objects; + private readonly WeaponLoading _weapons; + private readonly HumanModelList _humans; + private readonly DesignMerger _designMerger; + private readonly IClientState _clientState; - private ActorState? _jobChangeState; - private readonly Dictionary _jobChangeMainhand = []; - private readonly Dictionary _jobChangeOffhand = []; + private readonly JobChangeState _jobChangeState; - private void ResetJobChange() - { - _jobChangeState = null; - _jobChangeMainhand.Clear(); - _jobChangeOffhand.Clear(); - } - - public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, - CustomizeService customizations, ActorManager actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, + public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors, AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, - EquippedGearset equippedGearset) + EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState) { - _config = config; - _manager = manager; - _state = state; - _jobs = jobs; - _customizations = customizations; - _actors = actors; - _itemUnlocks = itemUnlocks; - _customizeUnlocks = customizeUnlocks; - _event = @event; - _objects = objects; - _weapons = weapons; - _humans = humans; - _clientState = clientState; - _equippedGearset = equippedGearset; - _jobs.JobChanged += OnJobChange; + _config = config; + _manager = manager; + _state = state; + _jobs = jobs; + _actors = actors; + _event = @event; + _objects = objects; + _weapons = weapons; + _humans = humans; + _clientState = clientState; + _equippedGearset = equippedGearset; + _designMerger = designMerger; + _jobChangeState = jobChangeState; + _jobs.JobChanged += OnJobChange; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier); _weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier); _equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier); @@ -78,45 +63,46 @@ public class AutoDesignApplier : IDisposable private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon) { - if (_jobChangeState == null || !_config.EnableAutoDesigns) + if (!_jobChangeState.HasState || !_config.EnableAutoDesigns) return; var id = actor.GetIdentifier(_actors); if (id == _jobChangeState.Identifier) { - var current = _jobChangeState.BaseData.Item(slot); + var state = _jobChangeState.State!; + var current = state.BaseData.Item(slot); switch (slot) { case EquipSlot.MainHand: { - if (_jobChangeMainhand.TryGetValue(current.Type, out var data)) + if (_jobChangeState.TryGetValue(current.Type, out var data)) { Glamourer.Log.Verbose( - $"Changing Mainhand from {_jobChangeState.ModelData.Weapon(EquipSlot.MainHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); - _state.ChangeItem(_jobChangeState, EquipSlot.MainHand, data.Item1, data.Item2); - weapon = _jobChangeState.ModelData.Weapon(EquipSlot.MainHand); + $"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); + _state.ChangeItem(_jobChangeState, EquipSlot.MainHand, data.Item1, new ApplySettings(Source: data.Item2)); + weapon = state.ModelData.Weapon(EquipSlot.MainHand); } break; } - case EquipSlot.OffHand when current.Type == _jobChangeState.BaseData.MainhandType.Offhand(): + case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand(): { - if (_jobChangeOffhand.TryGetValue(current.Type, out var data)) + if (_jobChangeState.TryGetValue(current.Type, out var data)) { Glamourer.Log.Verbose( - $"Changing Offhand from {_jobChangeState.ModelData.Weapon(EquipSlot.OffHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); - _state.ChangeItem(_jobChangeState, EquipSlot.OffHand, data.Item1, data.Item2); - weapon = _jobChangeState.ModelData.Weapon(EquipSlot.OffHand); + $"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); + _state.ChangeItem(_jobChangeState, EquipSlot.OffHand, data.Item1, new ApplySettings(Source: data.Item2)); + weapon = state.ModelData.Weapon(EquipSlot.OffHand); } - ResetJobChange(); + _jobChangeState.Reset(); break; } } } else { - ResetJobChange(); + _jobChangeState.Reset(); } } @@ -134,7 +120,7 @@ public class AutoDesignApplier : IDisposable break; case AutomationChanged.Type.ChangeIdentifier when set.Enabled: // Remove fixed state from the old identifiers assigned and the old enabled set, if any. - var (oldIds, _, oldSet) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!; + var (oldIds, _, _) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!; RemoveOld(oldIds); ApplyNew(set); // Does not need to disable oldSet because same identifiers. break; @@ -182,7 +168,7 @@ public class AutoDesignApplier : IDisposable } else if (_state.TryGetValue(id, out var state)) { - state.RemoveFixedDesignSources(); + state.Sources.RemoveFixedDesignSources(); } } } @@ -196,9 +182,9 @@ public class AutoDesignApplier : IDisposable { if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld) foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value)) - state.RemoveFixedDesignSources(); + state.Sources.RemoveFixedDesignSources(); else if (_state.TryGetValue(id, out var state)) - state.RemoveFixedDesignSources(); + state.Sources.RemoveFixedDesignSources(); } } } @@ -255,56 +241,31 @@ public class AutoDesignApplier : IDisposable else if (!GetPlayerSet(identifier, out set!)) { if (state.UpdateTerritory(_clientState.TerritoryType) && _config.RevertManualChangesOnZoneChange) - _state.ResetState(state, StateChanged.Source.Game); + _state.ResetState(state, StateSource.Game); return true; } var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange; if (!respectManual) - _state.ResetState(state, StateChanged.Source.Game); + _state.ResetState(state, StateSource.Game); Reduce(actor, state, set, respectManual, false); return true; } private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange) { - EquipFlag totalEquipFlags = 0; - CustomizeFlag totalCustomizeFlags = 0; - CrestFlag totalCrestFlags = 0; - CustomizeParameterFlag totalParameterFlags = 0; - byte totalMetaFlags = 0; if (set.BaseState == AutoDesignSet.Base.Game) _state.ResetStateFixed(state, respectManual); else if (!respectManual) - state.RemoveFixedDesignSources(); + state.Sources.RemoveFixedDesignSources(); if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return; - foreach (var design in set.Designs) - { - if (!design.IsActive(actor)) - continue; - - if (design.ApplicationType is 0) - continue; - - ref readonly var data = ref design.GetDesignData(state); - var source = design.Revert ? StateChanged.Source.Game : StateChanged.Source.Fixed; - - if (!data.IsHuman) - continue; - - var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat(); - ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source); - ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source); - ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange); - ReduceCrests(state, data, crestFlags, ref totalCrestFlags, respectManual, source); - ReduceParameters(state, data, parameterFlags, ref totalParameterFlags, respectManual, source); - } - - if (totalCustomizeFlags != 0) - state.ModelData.ModelId = 0; + var mergedDesign = _designMerger.Merge( + set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design?.AllLinks ?? [(d.Design, d.Type)]), + state.ModelData, true, false); + _state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false)); } /// Get world-specific first and all-world afterward. @@ -330,221 +291,6 @@ public class AutoDesignApplier : IDisposable } } - private void ReduceCrests(ActorState state, in DesignData design, CrestFlag crestFlags, ref CrestFlag totalCrestFlags, bool respectManual, - StateChanged.Source source) - { - crestFlags &= ~totalCrestFlags; - if (crestFlags == 0) - return; - - foreach (var slot in CrestExtensions.AllRelevantSet) - { - if (!crestFlags.HasFlag(slot)) - continue; - - if (!respectManual || state[slot] is not StateChanged.Source.Manual) - _state.ChangeCrest(state, slot, design.Crest(slot), source); - totalCrestFlags |= slot; - } - } - - private void ReduceParameters(ActorState state, in DesignData design, CustomizeParameterFlag parameterFlags, - ref CustomizeParameterFlag totalParameterFlags, bool respectManual, StateChanged.Source source) - { - parameterFlags &= ~totalParameterFlags; - if (parameterFlags == 0) - return; - - foreach (var flag in CustomizeParameterExtensions.AllFlags) - { - if (!parameterFlags.HasFlag(flag)) - continue; - - if (!respectManual || state[flag] is not StateChanged.Source.Manual and not StateChanged.Source.Pending) - _state.ChangeCustomizeParameter(state, flag, design.Parameters[flag], source); - totalParameterFlags |= flag; - } - } - - private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual, - StateChanged.Source source, bool fromJobChange) - { - equipFlags &= ~totalEquipFlags; - if (equipFlags == 0) - return; - - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var flag = slot.ToFlag(); - if (equipFlags.HasFlag(flag)) - { - var item = design.Item(slot); - if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _)) - { - if (!respectManual || state[slot, false] is not StateChanged.Source.Manual) - _state.ChangeItem(state, slot, item, source); - totalEquipFlags |= flag; - } - } - - var stainFlag = slot.ToStainFlag(); - if (equipFlags.HasFlag(stainFlag)) - { - if (!respectManual || state[slot, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, slot, design.Stain(slot), source); - totalEquipFlags |= stainFlag; - } - } - - 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; - if (checkUnlock && checkState) - { - if (fromJobChange) - { - _jobChangeMainhand.TryAdd(item.Type, (item, source)); - _jobChangeState = state; - } - else if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type) - { - _state.ChangeItem(state, EquipSlot.MainHand, item, source); - totalEquipFlags |= EquipFlag.Mainhand; - } - } - } - - if (equipFlags.HasFlag(EquipFlag.Offhand)) - { - 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; - if (checkUnlock && checkState) - { - if (fromJobChange) - { - _jobChangeOffhand.TryAdd(item.Type, (item, source)); - _jobChangeState = state; - } - else if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type) - { - _state.ChangeItem(state, EquipSlot.OffHand, item, source); - totalEquipFlags |= EquipFlag.Offhand; - } - } - } - - if (equipFlags.HasFlag(EquipFlag.MainhandStain)) - { - if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source); - totalEquipFlags |= EquipFlag.MainhandStain; - } - - if (equipFlags.HasFlag(EquipFlag.OffhandStain)) - { - if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source); - totalEquipFlags |= EquipFlag.OffhandStain; - } - } - - private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags, - bool respectManual, StateChanged.Source source) - { - customizeFlags &= ~totalCustomizeFlags; - if (customizeFlags == 0) - return; - - var customize = state.ModelData.Customize; - CustomizeFlag fixFlags = 0; - - // Skip anything not human. - if (!state.ModelData.IsHuman || !design.IsHuman) - return; - - if (customizeFlags.HasFlag(CustomizeFlag.Clan)) - { - if (!respectManual || state[CustomizeIndex.Clan] is not StateChanged.Source.Manual) - fixFlags |= _customizations.ChangeClan(ref customize, design.Customize.Clan); - customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race); - totalCustomizeFlags |= CustomizeFlag.Clan | CustomizeFlag.Race; - } - - if (customizeFlags.HasFlag(CustomizeFlag.Gender)) - { - if (!respectManual || state[CustomizeIndex.Gender] is not StateChanged.Source.Manual) - fixFlags |= _customizations.ChangeGender(ref customize, design.Customize.Gender); - customizeFlags &= ~CustomizeFlag.Gender; - totalCustomizeFlags |= CustomizeFlag.Gender; - } - - if (fixFlags != 0) - _state.ChangeCustomize(state, customize, fixFlags, source); - - if (customizeFlags.HasFlag(CustomizeFlag.Face)) - { - if (!respectManual || state[CustomizeIndex.Face] is not StateChanged.Source.Manual) - _state.ChangeCustomize(state, CustomizeIndex.Face, design.Customize.Face, source); - customizeFlags &= ~CustomizeFlag.Face; - totalCustomizeFlags |= CustomizeFlag.Face; - } - - var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); - var face = state.ModelData.Customize.Face; - foreach (var index in Enum.GetValues()) - { - var flag = index.ToFlag(); - if (!customizeFlags.HasFlag(flag)) - continue; - - var value = design.Customize[index]; - if (CustomizeService.IsCustomizationValid(set, face, index, value, out var data)) - { - if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _)) - continue; - - if (!respectManual || state[index] is not StateChanged.Source.Manual) - _state.ChangeCustomize(state, index, value, source); - totalCustomizeFlags |= flag; - } - } - } - - private void ReduceMeta(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet, - ref byte totalMetaFlags, bool respectManual, StateChanged.Source source) - { - if (applyHat && (totalMetaFlags & 0x01) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual) - _state.ChangeHatState(state, design.IsHatVisible(), source); - totalMetaFlags |= 0x01; - } - - if (applyVisor && (totalMetaFlags & 0x02) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual) - _state.ChangeVisorState(state, design.IsVisorToggled(), source); - totalMetaFlags |= 0x02; - } - - if (applyWeapon && (totalMetaFlags & 0x04) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual) - _state.ChangeWeaponState(state, design.IsWeaponVisible(), source); - totalMetaFlags |= 0x04; - } - - if (applyWet && (totalMetaFlags & 0x08) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual) - _state.ChangeWetness(state, design.IsWet(), source); - totalMetaFlags |= 0x08; - } - } - internal static int NewGearsetId = -1; private void OnEquippedGearset(string name, int id, int prior, byte _, byte job) diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 38e4479..e45b8e0 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -231,9 +231,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos { var newDesign = new AutoDesign() { - Design = design, - ApplicationType = AutoDesign.Type.All, - Jobs = _jobs.JobGroups[1], + Design = design, + Type = ApplicationType.All, + Jobs = _jobs.JobGroups[1], }; set.Designs.Add(newDesign); Save(); @@ -328,21 +328,21 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index)); } - public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type) + public void ChangeApplicationType(AutoDesignSet set, int which, ApplicationType applicationType) { if (which >= set.Designs.Count || which < 0) return; - type &= AutoDesign.Type.All; + applicationType &= ApplicationType.All; var design = set.Designs[which]; - if (design.ApplicationType == type) + if (design.Type == applicationType) return; - var old = design.ApplicationType; - design.ApplicationType = type; + var old = design.Type; + design.Type = applicationType; Save(); - Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set."); - _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type)); + Glamourer.Log.Debug($"Changed application type from {old} to {applicationType} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, applicationType)); } public string ToFilename(FilenameService fileNames) @@ -480,8 +480,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos return null; } - design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid); - if (design == null) + if (!_designs.Designs.TryGetValue(guid, out design)) { Glamourer.Messager.NotificationMessage( $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", @@ -490,12 +489,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos } } - var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject() ?? 0); + // ApplicationType is a migration from an older property name. + var applicationType = (ApplicationType)(jObj["Type"]?.ToObject() ?? jObj["ApplicationType"]?.ToObject() ?? 0); - var ret = new AutoDesign() + var ret = new AutoDesign { - Design = design, - ApplicationType = applicationType & AutoDesign.Type.All, + Design = design, + Type = applicationType & ApplicationType.All, }; return ParseConditions(setName, jObj, ret) ? ret : null; } @@ -550,7 +550,24 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos private ActorIdentifier[] GetGroup(ActorIdentifier identifier) { if (!identifier.IsValid) - return Array.Empty(); + return []; + + return identifier.Type switch + { + IdentifierType.Player => + [ + identifier.CreatePermanent(), + ], + IdentifierType.Retainer => + [ + _actors.CreateRetainer(identifier.PlayerName, + identifier.Retainer == ActorIdentifier.RetainerType.Mannequin + ? ActorIdentifier.RetainerType.Mannequin + : ActorIdentifier.RetainerType.Bell).CreatePermanent(), + ], + IdentifierType.Npc => CreateNpcs(_actors, identifier), + _ => [], + }; static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) { @@ -566,23 +583,6 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos identifier.Kind, kvp.Key)).ToArray(); } - - return identifier.Type switch - { - IdentifierType.Player => new[] - { - identifier.CreatePermanent(), - }, - IdentifierType.Retainer => new[] - { - _actors.CreateRetainer(identifier.PlayerName, - identifier.Retainer == ActorIdentifier.RetainerType.Mannequin - ? ActorIdentifier.RetainerType.Mannequin - : ActorIdentifier.RetainerType.Bell).CreatePermanent(), - }, - IdentifierType.Npc => CreateNpcs(_actors, identifier), - _ => Array.Empty(), - }; } private void OnDesignChange(DesignChanged.Type type, Design design, object? data) diff --git a/Glamourer/Automation/AutoDesignSet.cs b/Glamourer/Automation/AutoDesignSet.cs index 3e1d713..29a0e2e 100644 --- a/Glamourer/Automation/AutoDesignSet.cs +++ b/Glamourer/Automation/AutoDesignSet.cs @@ -3,12 +3,12 @@ using Penumbra.GameData.Actors; namespace Glamourer.Automation; -public class AutoDesignSet +public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List designs) { - public readonly List Designs; + public readonly List Designs = designs; - public string Name; - public ActorIdentifier[] Identifiers; + public string Name = name; + public ActorIdentifier[] Identifiers = identifiers; public bool Enabled; public Base BaseState = Base.Current; @@ -32,13 +32,6 @@ public class AutoDesignSet : this(name, identifiers, new List()) { } - public AutoDesignSet(string name, ActorIdentifier[] identifiers, List designs) - { - Name = name; - Identifiers = identifiers; - Designs = designs; - } - public enum Base : byte { Current, diff --git a/Glamourer/Automation/FixedDesignMigrator.cs b/Glamourer/Automation/FixedDesignMigrator.cs index 9809fd8..856561a 100644 --- a/Glamourer/Automation/FixedDesignMigrator.cs +++ b/Glamourer/Automation/FixedDesignMigrator.cs @@ -56,7 +56,7 @@ public class FixedDesignMigrator(JobService jobs) autoManager.AddDesign(set, leaf.Value); autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2); - autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? AutoDesign.Type.All : 0); + autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? ApplicationType.All : 0); } } } diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 9e01d18..78d1a3b 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -41,6 +41,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseFloatForColors { get; set; } = true; public bool UseRgbForColors { get; set; } = true; public bool ShowColorConfig { get; set; } = true; + public bool ChangeEntireItem { get; set; } = false; public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY); public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 4ff6f2d..89dd62f 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,4 +1,6 @@ using Dalamud.Interface.Internal.Notifications; +using Glamourer.Automation; +using Glamourer.Designs.Links; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Newtonsoft.Json; @@ -35,14 +37,18 @@ public sealed class Design : DesignBase, ISavable public DateTimeOffset LastEdit { get; internal set; } public LowerString Name { get; internal set; } = LowerString.Empty; public string Description { get; internal set; } = string.Empty; - public string[] Tags { get; internal set; } = Array.Empty(); + public string[] Tags { get; internal set; } = []; public int Index { get; internal set; } public string Color { get; internal set; } = string.Empty; - public SortedList AssociatedMods { get; private set; } = new(); + public SortedList AssociatedMods { get; private set; } = []; + public LinkContainer Links { get; private set; } = []; public string Incognito => Identifier.ToString()[..8]; + public IEnumerable<(DesignBase? Design, ApplicationType Flags)> AllLinks + => LinkContainer.GetAllLinks(this).Select(t => ((DesignBase?)t.Link.Link, t.Link.Type)); + #endregion #region Serialization @@ -64,6 +70,7 @@ public sealed class Design : DesignBase, ISavable ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), ["Mods"] = SerializeMods(), + ["Links"] = Links.Serialize(), }; return ret; } @@ -95,24 +102,18 @@ public sealed class Design : DesignBase, ISavable #region Deserialization - public static Design LoadDesign(CustomizeService customizations, ItemManager items, JObject json) + public static Design LoadDesign(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch { - FileVersion => LoadDesignV1(customizations, items, json), + FileVersion => LoadDesignV1(customizations, items, linkLoader, json), _ => throw new Exception("The design to be loaded has no valid Version."), }; } - private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, JObject json) + private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) { - static string[] ParseTags(JObject json) - { - var tags = json["Tags"]?.ToObject() ?? Array.Empty(); - return tags.OrderBy(t => t).Distinct().ToArray(); - } - var creationDate = json["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate"); var design = new Design(customizations, items) @@ -131,8 +132,15 @@ public sealed class Design : DesignBase, ISavable LoadEquip(items, json["Equipment"], design, design.Name, true); LoadMods(json["Mods"], design); LoadParameters(json["Parameters"], design, design.Name); + LoadLinks(linkLoader, json["Links"], design); design.Color = json["Color"]?.ToObject() ?? string.Empty; return design; + + static string[] ParseTags(JObject json) + { + var tags = json["Tags"]?.ToObject() ?? Array.Empty(); + return tags.OrderBy(t => t).Distinct().ToArray(); + } } private static void LoadMods(JToken? mods, Design design) @@ -161,6 +169,29 @@ public sealed class Design : DesignBase, ISavable } } + private static void LoadLinks(DesignLinkLoader linkLoader, JToken? links, Design design) + { + if (links is not JObject obj) + return; + + Parse(obj["Before"] as JArray, LinkOrder.Before); + Parse(obj["After"] as JArray, LinkOrder.After); + return; + + void Parse(JArray? array, LinkOrder order) + { + if (array == null) + return; + + foreach (var obj in array.OfType()) + { + var identifier = obj["Design"]?.ToObject() ?? throw new ArgumentNullException("Design"); + var type = (ApplicationType)(obj["Type"]?.ToObject() ?? 0); + linkLoader.AddObject(design, new LinkData(identifier, type, order)); + } + } + } + #endregion #region ISavable diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 2363060..a533bc4 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Glamourer.GameData; using Glamourer.Services; +using Glamourer.State; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Enums; @@ -34,7 +35,7 @@ public class DesignBase _designData = designData; ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; ApplyEquip = equipFlags & EquipFlagExtensions.All; - _designFlags = 0; + ApplyMeta = 0; CustomizeSet = SetCustomizationSet(customize); } @@ -45,7 +46,7 @@ public class DesignBase ApplyCustomize = clone.ApplyCustomizeRaw; ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; ApplyParameters = clone.ApplyParameters & CustomizeParameterExtensions.All; - _designFlags = clone._designFlags & (DesignFlags)0x0F; + ApplyMeta = clone.ApplyMeta & MetaExtensions.All; } /// Ensure that the customization set is updated when the design data changes. @@ -57,16 +58,6 @@ public class DesignBase #region Application Data - [Flags] - private enum DesignFlags : byte - { - ApplyHatVisible = 0x01, - ApplyVisorState = 0x02, - ApplyWeaponVisible = 0x04, - ApplyWetness = 0x08, - WriteProtected = 0x10, - } - private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; public CustomizeSet CustomizeSet { get; private set; } @@ -84,9 +75,10 @@ public class DesignBase internal CustomizeFlag ApplyCustomizeRaw => _applyCustomize; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; - private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; + internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; + internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; + private bool _writeProtected; public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) { @@ -98,69 +90,30 @@ public class DesignBase return true; } - public bool DoApplyHatVisible() - => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); - - public bool DoApplyVisorToggle() - => _designFlags.HasFlag(DesignFlags.ApplyVisorState); - - public bool DoApplyWeaponVisible() - => _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible); - - public bool DoApplyWetness() - => _designFlags.HasFlag(DesignFlags.ApplyWetness); + public bool DoApplyMeta(MetaIndex index) + => ApplyMeta.HasFlag(index.ToFlag()); public bool WriteProtected() - => _designFlags.HasFlag(DesignFlags.WriteProtected); + => _writeProtected; - public bool SetApplyHatVisible(bool value) + public bool SetApplyMeta(MetaIndex index, bool value) { - var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible; - if (newFlag == _designFlags) + var newFlag = value ? ApplyMeta | index.ToFlag() : ApplyMeta & ~index.ToFlag(); + if (newFlag == ApplyMeta) return false; - _designFlags = newFlag; - return true; - } - - public bool SetApplyVisorToggle(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWeaponVisible(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWetness(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; + ApplyMeta = newFlag; return true; } public bool SetWriteProtected(bool value) { - var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected; - if (newFlag == _designFlags) + if (value == _writeProtected) return false; - _designFlags = newFlag; + _writeProtected = value; return true; + } public bool DoApplyEquip(EquipSlot slot) @@ -298,9 +251,9 @@ public class DesignBase ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); } - ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); - ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply"); - ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply"); + ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply"); + ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply"); + ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply"); } else { @@ -344,7 +297,7 @@ public class DesignBase ret["Wetness"] = new JObject() { ["Value"] = _designData.IsWet(), - ["Apply"] = DoApplyWetness(), + ["Apply"] = DoApplyMeta(MetaIndex.Wetness), }; return ret; @@ -478,7 +431,7 @@ public class DesignBase // Load the token and set application. bool TryGetToken(CustomizeParameterFlag flag, [NotNullWhen(true)] out JToken? token) { - token = parameters![flag.ToString()]; + token = parameters[flag.ToString()]; if (token != null) { var apply = token["Apply"]?.ToObject() ?? false; @@ -493,8 +446,8 @@ public class DesignBase void MigrateLipOpacity() { - var token = parameters!["LipOpacity"]?["Percentage"]?.ToObject(); - var actualToken = parameters![CustomizeParameterFlag.LipDiffuse.ToString()]?["Alpha"]; + var token = parameters["LipOpacity"]?["Percentage"]?.ToObject(); + var actualToken = parameters[CustomizeParameterFlag.LipDiffuse.ToString()]?["Alpha"]; if (token != null && actualToken == null) design.GetDesignDataRef().Parameters.LipDiffuse.W = token.Value; } @@ -575,15 +528,15 @@ public class DesignBase design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff); } var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyHatVisible(metaValue.Enabled); + design.SetApplyMeta(MetaIndex.HatState, metaValue.Enabled); design._designData.SetHatVisible(metaValue.ForcedValue); metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyWeaponVisible(metaValue.Enabled); + design.SetApplyMeta(MetaIndex.WeaponState, metaValue.Enabled); design._designData.SetWeaponVisible(metaValue.ForcedValue); metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); - design.SetApplyVisorToggle(metaValue.Enabled); + design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled); design._designData.SetVisor(metaValue.ForcedValue); } @@ -610,7 +563,7 @@ public class DesignBase var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse); design._designData.SetIsWet(wetness.ForcedValue); - design.SetApplyWetness(wetness.Enabled); + design.SetApplyMeta(MetaIndex.Wetness, wetness.Enabled); design._designData.ModelId = json["ModelId"]?.ToObject() ?? 0; PrintWarning(customizations.ValidateModelId(design._designData.ModelId, out design._designData.ModelId, @@ -664,17 +617,13 @@ public class DesignBase try { _designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags, - out var writeProtected, - out var applyHat, out var applyVisor, out var applyWeapon); + out var writeProtected, out var applyMeta); ApplyEquip = equipFlags; ApplyCustomize = customizeFlags; ApplyParameters = 0; ApplyCrest = 0; + ApplyMeta = applyMeta; SetWriteProtected(writeProtected); - SetApplyHatVisible(applyHat); - SetApplyVisorToggle(applyVisor); - SetApplyWeaponVisible(applyWeapon); - SetApplyWetness(true); CustomizeSet = SetCustomizationSet(customize); } catch (Exception ex) diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs index 2d85924..a8b2f7b 100644 --- a/Glamourer/Designs/DesignBase64Migration.cs +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -1,4 +1,5 @@ using Glamourer.Services; +using Glamourer.State; using OtterGui; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; @@ -13,7 +14,7 @@ public class DesignBase64Migration public const int Base64SizeV4 = 95; public static unsafe DesignData MigrateBase64(ItemManager items, HumanModelList humans, string base64, out EquipFlag equipFlags, - out CustomizeFlag customizeFlags, out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon) + out CustomizeFlag customizeFlags, out bool writeProtected, out MetaFlag metaFlags) { static void CheckSize(int length, int requiredLength) { @@ -25,9 +26,7 @@ public class DesignBase64Migration byte applicationFlags; ushort equipFlagsS; var bytes = Convert.FromBase64String(base64); - applyHat = false; - applyVisor = false; - applyWeapon = false; + metaFlags = MetaFlag.Wetness; var data = new DesignData(); switch (bytes[0]) { @@ -77,9 +76,12 @@ public class DesignBase64Migration customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; data.SetIsWet((applicationFlags & 0x02) != 0); - applyHat = (applicationFlags & 0x04) != 0; - applyWeapon = (applicationFlags & 0x08) != 0; - applyVisor = (applicationFlags & 0x10) != 0; + if ((applicationFlags & 0x04) != 0) + metaFlags |= MetaFlag.HatState; + if ((applicationFlags & 0x08) != 0) + metaFlags |= MetaFlag.WeaponState; + if ((applicationFlags & 0x10) != 0) + metaFlags |= MetaFlag.VisorState; writeProtected = (applicationFlags & 0x20) != 0; equipFlags = 0; @@ -161,16 +163,15 @@ public class DesignBase64Migration } } - public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, - bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f) + public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta, bool writeProtected, float alpha = 1.0f) { var data = stackalloc byte[Base64SizeV4]; data[0] = 5; data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0) | (save.IsWet() ? 0x02 : 0) - | (setHat ? 0x04 : 0) - | (setWeapon ? 0x08 : 0) - | (setVisor ? 0x10 : 0) + | (meta.HasFlag(MetaFlag.HatState) ? 0x04 : 0) + | (meta.HasFlag(MetaFlag.WeaponState) ? 0x08 : 0) + | (meta.HasFlag(MetaFlag.VisorState) ? 0x10 : 0) | (writeProtected ? 0x20 : 0)); data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0) | (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0) diff --git a/Glamourer/Designs/DesignColors.cs b/Glamourer/Designs/DesignColors.cs index 4026a98..bd192be 100644 --- a/Glamourer/Designs/DesignColors.cs +++ b/Glamourer/Designs/DesignColors.cs @@ -11,21 +11,10 @@ using OtterGui.Classes; namespace Glamourer.Designs; -public class DesignColorUi +public class DesignColorUi(DesignColors colors, Configuration config) { - private readonly DesignColors _colors; - private readonly DesignManager _designs; - private readonly Configuration _config; - private string _newName = string.Empty; - public DesignColorUi(DesignColors colors, DesignManager designs, Configuration config) - { - _colors = colors; - _designs = designs; - _config = config; - } - public void Draw() { using var table = ImRaii.Table("designColors", 3, ImGuiTableFlags.RowBg); @@ -44,7 +33,7 @@ public class DesignColorUi ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), buttonSize, - "Revert the color used for missing design colors to its default.", _colors.MissingColor == DesignColors.MissingColorDefault, + "Revert the color used for missing design colors to its default.", colors.MissingColor == DesignColors.MissingColorDefault, true)) { changeString = DesignColors.MissingColorName; @@ -52,7 +41,7 @@ public class DesignColorUi } ImGui.TableNextColumn(); - if (DrawColorButton(DesignColors.MissingColorName, _colors.MissingColor, out var newColor)) + if (DrawColorButton(DesignColors.MissingColorName, colors.MissingColor, out var newColor)) { changeString = DesignColors.MissingColorName; changeValue = newColor; @@ -64,12 +53,12 @@ public class DesignColorUi ImGuiUtil.HoverTooltip("This color is used when the color specified in a design is not available."); - var disabled = !_config.DeleteDesignModifier.IsActive(); + var disabled = !config.DeleteDesignModifier.IsActive(); var tt = "Delete this color. This does not remove it from designs using it."; if (disabled) - tt += $"\nHold {_config.DeleteDesignModifier} to delete."; + tt += $"\nHold {config.DeleteDesignModifier} to delete."; - foreach (var ((name, color), idx) in _colors.WithIndex()) + foreach (var ((name, color), idx) in colors.WithIndex()) { using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); @@ -97,7 +86,7 @@ public class DesignColorUi ? ("Specify a name for a new color first.", true) : _newName is DesignColors.MissingColorName or DesignColors.AutomaticName ? ($"You can not use the name {DesignColors.MissingColorName} or {DesignColors.AutomaticName}, choose a different one.", true) - : _colors.ContainsKey(_newName) + : colors.ContainsKey(_newName) ? ($"The color {_newName} already exists, please choose a different name.", true) : ($"Add a new color {_newName} to your list.", false); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, disabled, true)) @@ -119,9 +108,9 @@ public class DesignColorUi if (changeString.Length > 0) { if (!changeValue.HasValue) - _colors.DeleteColor(changeString); + colors.DeleteColor(changeString); else - _colors.SetColor(changeString, changeValue.Value); + colors.SetColor(changeString, changeValue.Value); } } diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index 949fa06..14c85f6 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -1,4 +1,5 @@ -using Glamourer.GameData; +using Glamourer.Designs.Links; +using Glamourer.GameData; using Glamourer.Services; using Glamourer.State; using Glamourer.Utility; @@ -10,7 +11,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans) +public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans, DesignLinkLoader _linkLoader) { public const byte Version = 6; @@ -54,10 +55,10 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; design.ApplyCrest = crestFlags & CrestExtensions.All; design.ApplyParameters = parameterFlags & CustomizeParameterExtensions.All; - design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); - design.SetApplyWetness(true); + design.SetApplyMeta(MetaIndex.HatState, design.DoApplyEquip(EquipSlot.Head)); + design.SetApplyMeta(MetaIndex.VisorState, design.DoApplyEquip(EquipSlot.Head)); + design.SetApplyMeta(MetaIndex.WeaponState, design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); + design.SetApplyMeta(MetaIndex.Wetness, true); design.SetDesignData(_customize, data); return design; } @@ -75,7 +76,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi case (byte)'{': var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes)); ret = jObj1["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj1) + ? Design.LoadDesign(_customize, _items, _linkLoader, jObj1) : DesignBase.LoadDesignBase(_customize, _items, jObj1); break; case 1: @@ -90,7 +91,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 3); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj2) + ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -101,7 +102,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 5); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj2) + ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -111,7 +112,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 6); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj2) + ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -125,16 +126,14 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi return null; } - ret.SetApplyWetness(customize); + ret.SetApplyMeta(MetaIndex.Wetness, customize); ret.ApplyCustomize = customize ? CustomizeFlagExtensions.AllRelevant : 0; if (!equip) { - ret.ApplyEquip = 0; - ret.ApplyCrest = 0; - ret.SetApplyHatVisible(false); - ret.SetApplyWeaponVisible(false); - ret.SetApplyVisorToggle(false); + ret.ApplyEquip = 0; + ret.ApplyCrest = 0; + ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState); } return ret; diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index edc35e1..6b84768 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -62,10 +62,10 @@ public unsafe struct DesignData => CrestVisibility.HasFlag(slot); - public FullEquipType MainhandType + public readonly FullEquipType MainhandType => _typeMainhand; - public FullEquipType OffhandType + public readonly FullEquipType OffhandType => _typeOffhand; public readonly EquipItem Item(EquipSlot slot) @@ -186,6 +186,26 @@ public unsafe struct DesignData return true; } + public readonly bool GetMeta(MetaIndex index) + => index switch + { + MetaIndex.Wetness => IsWet(), + MetaIndex.HatState => IsHatVisible(), + MetaIndex.VisorState => IsVisorToggled(), + MetaIndex.WeaponState => IsWeaponVisible(), + _ => false, + }; + + public bool SetMeta(MetaIndex index, bool value) + => index switch + { + MetaIndex.Wetness => SetIsWet(value), + MetaIndex.HatState => SetHatVisible(value), + MetaIndex.VisorState => SetVisor(value), + MetaIndex.WeaponState => SetWeaponVisible(value), + _ => false, + }; + public readonly bool IsWet() => (_states & 0x01) == 0x01; diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs new file mode 100644 index 0000000..ab258d7 --- /dev/null +++ b/Glamourer/Designs/DesignEditor.cs @@ -0,0 +1,297 @@ +using Glamourer.Designs.Links; +using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Services; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class DesignEditor( + SaveService saveService, + DesignChanged designChanged, + CustomizeService customizations, + ItemManager items, + Configuration config) + : IDesignEditor +{ + protected readonly DesignChanged DesignChanged = designChanged; + protected readonly SaveService SaveService = saveService; + protected readonly ItemManager Items = items; + protected readonly CustomizeService Customizations = customizations; + protected readonly Configuration Config = config; + protected readonly Dictionary UndoStore = []; + + private bool _forceFullItemOff; + + /// Whether an Undo for the given design is possible. + public bool CanUndo(Design? design) + => design != null && UndoStore.ContainsKey(design.Identifier); + + /// + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings _ = default) + { + var design = (Design)data; + var oldValue = design.DesignData.Customize[idx]; + switch (idx) + { + case CustomizeIndex.Race: + case CustomizeIndex.BodyType: + Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); + return; + case CustomizeIndex.Clan: + { + var customize = design.DesignData.Customize; + if (Customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0) + return; + if (!design.SetCustomize(Customizations, customize)) + return; + + break; + } + case CustomizeIndex.Gender: + { + var customize = design.DesignData.Customize; + if (Customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0) + return; + if (!design.SetCustomize(Customizations, customize)) + return; + + break; + } + default: + if (!Customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender, + design.DesignData.Customize.Face, idx, value) + || !design.GetDesignDataRef().Customize.Set(idx, value)) + return; + + break; + } + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); + } + + /// + public void ChangeEntireCustomize(object data, in CustomizeArray customize, CustomizeFlag apply, ApplySettings _ = default) + { + var design = (Design)data; + var (newCustomize, applied, changed) = Customizations.Combine(design.DesignData.Customize, customize, apply, true); + if (changed == 0) + return; + + var oldCustomize = design.DesignData.Customize; + design.SetCustomize(Customizations, newCustomize); + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, (oldCustomize, applied, changed)); + } + + /// + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings _ = default) + { + var design = (Design)data; + var old = design.DesignData.Parameters[flag]; + if (!design.GetDesignDataRef().Parameters.Set(flag, value)) + return; + + var @new = design.DesignData.Parameters[flag]; + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag)); + } + + /// + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings _ = default) + { + var design = (Design)data; + switch (slot) + { + case EquipSlot.MainHand: + { + var currentMain = design.DesignData.Item(EquipSlot.MainHand); + var currentOff = design.DesignData.Item(EquipSlot.OffHand); + if (!Items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item)) + return; + + if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff, newGauntlets)); + return; + } + case EquipSlot.OffHand: + { + var currentMain = design.DesignData.Item(EquipSlot.MainHand); + var currentOff = design.DesignData.Item(EquipSlot.OffHand); + if (!Items.IsOffhandValid(currentOff.Type, item.ItemId, out item)) + return; + + if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); + return; + } + default: + { + if (!Items.IsItemValid(slot, item.Id, out item)) + return; + + var old = design.DesignData.Item(slot); + if (!design.GetDesignDataRef().SetItem(slot, item)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug( + $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); + return; + } + } + } + + /// + public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings _ = default) + { + var design = (Design)data; + if (Items.ValidateStain(stain, out var _, false).Length > 0) + return; + + var oldStain = design.DesignData.Stain(slot); + if (!design.GetDesignDataRef().SetStain(slot, stain)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}."); + DesignChanged.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + } + + /// + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings _ = default) + { + if (item.HasValue) + ChangeItem(data, slot, item.Value, _); + if (stain.HasValue) + ChangeStain(data, slot, stain.Value, _); + } + + /// + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings _ = default) + { + var design = (Design)data; + 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}."); + DesignChanged.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); + } + + /// + public void ChangeMetaState(object data, MetaIndex metaIndex, bool value, ApplySettings _ = default) + { + var design = (Design)data; + if (!design.GetDesignDataRef().SetMeta(metaIndex, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); + DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + } + + /// + public void ApplyDesign(object data, MergedDesign other, ApplySettings _ = default) + => ApplyDesign(data, other.Design); + + /// + public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default) + { + var design = (Design)data; + UndoStore[design.Identifier] = design.DesignData; + foreach (var index in MetaExtensions.AllRelevant.Where(other.DoApplyMeta)) + design.GetDesignDataRef().SetMeta(index, other.DesignData.GetMeta(index)); + + if (!design.DesignData.IsHuman) + return; + + ChangeEntireCustomize(design, other.DesignData.Customize, other.ApplyCustomize); + + _forceFullItemOff = true; + foreach (var slot in EquipSlotExtensions.FullSlots) + { + ChangeEquip(design, slot, + other.DoApplyEquip(slot) ? other.DesignData.Item(slot) : null, + other.DoApplyStain(slot) ? other.DesignData.Stain(slot) : null); + } + + _forceFullItemOff = false; + + foreach (var slot in Enum.GetValues().Where(other.DoApplyCrest)) + ChangeCrest(design, slot, other.DesignData.Crest(slot)); + + foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter)) + ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]); + } + + /// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. + private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain, out EquipItem? newOff, + out EquipItem? newGauntlets) + { + newOff = null; + newGauntlets = null; + if (newMain.Type != currentMain.Type) + { + var defaultOffhand = Items.GetDefaultOffhand(newMain); + if (!Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) + return false; + + newOff = o; + } + else if (!_forceFullItemOff && Config.ChangeEntireItem) + { + var defaultOffhand = Items.GetDefaultOffhand(newMain); + if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) + newOff = o; + + if (newMain.Type is FullEquipType.Fists && Items.ItemData.Tertiary.TryGetValue(newMain.ItemId, out var g)) + newGauntlets = g; + } + + if (!design.GetDesignDataRef().SetItem(EquipSlot.MainHand, newMain)) + return false; + + if (newOff.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.OffHand, newOff.Value)) + { + design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain); + return false; + } + + if (newGauntlets.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.Hands, newGauntlets.Value)) + { + design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain); + design.GetDesignDataRef().SetItem(EquipSlot.OffHand, currentOff); + return false; + } + + return true; + } +} diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index 2f346a1..00277c2 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -105,7 +105,8 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable } catch (Exception ex) { - Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", NotificationType.Error); + Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", + NotificationType.Error); } CreateDuplicateLeaf(parent, design.Name.Text, design); diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index f16281a..c3d8664 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -1,65 +1,58 @@ using Dalamud.Utility; +using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; using Glamourer.Interop.Penumbra; using Glamourer.Services; -using Glamourer.State; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public class DesignManager +public sealed class DesignManager : DesignEditor { - private readonly CustomizeService _customizations; - private readonly ItemManager _items; - private readonly HumanModelList _humans; - private readonly SaveService _saveService; - private readonly DesignChanged _event; - private readonly List _designs = []; - private readonly Dictionary _undoStore = []; - - public IReadOnlyList Designs - => _designs; + public readonly DesignStorage Designs; + private readonly HumanModelList _humans; public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations, - DesignChanged @event, HumanModelList humans) + DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader, Configuration config) + : base(saveService, @event, customizations, items, config) { - _saveService = saveService; - _items = items; - _customizations = customizations; - _event = @event; - _humans = humans; + Designs = storage; + _humans = humans; + + LoadDesigns(designLinkLoader); CreateDesignFolder(saveService); - LoadDesigns(); MigrateOldDesigns(); + designLinkLoader.SetAllObjects(); } + #region Design Management + /// /// Clear currently loaded designs and load all designs anew from file. /// Invalid data is fixed, but changes are not saved until manual changes. /// - public void LoadDesigns() + private void LoadDesigns(DesignLinkLoader linkLoader) { _humans.Awaiter.Wait(); - _customizations.Awaiter.Wait(); - _items.ItemData.Awaiter.Wait(); + Customizations.Awaiter.Wait(); + Items.ItemData.Awaiter.Wait(); var stopwatch = Stopwatch.StartNew(); - _designs.Clear(); + Designs.Clear(); var skipped = 0; ThreadLocal> designs = new(() => [], true); - Parallel.ForEach(_saveService.FileNames.Designs(), (f, _) => + Parallel.ForEach(SaveService.FileNames.Designs(), (f, _) => { try { var text = File.ReadAllText(f.FullName); var data = JObject.Parse(text); - var design = Design.LoadDesign(_customizations, _items, data); + var design = Design.LoadDesign(Customizations, Items, linkLoader, data); designs.Value!.Add((design, f.FullName)); } catch (Exception ex) @@ -74,15 +67,15 @@ public class DesignManager { if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(path)) invalidNames.Add((design, path)); - if (_designs.Any(d => d.Identifier == design.Identifier)) + if (Designs.Contains(design.Identifier)) { Glamourer.Log.Error($"Could not load design, skipped: Identifier {design.Identifier} was not unique."); ++skipped; continue; } - design.Index = _designs.Count; - _designs.Add(design); + design.Index = Designs.Count; + Designs.Add(design); } var failed = MoveInvalidNames(invalidNames); @@ -91,34 +84,30 @@ public class DesignManager $"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}"); Glamourer.Log.Information( - $"Loaded {_designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); - _event.Invoke(DesignChanged.Type.ReloadedAll, null!, null); + $"Loaded {Designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); + DesignChanged.Invoke(DesignChanged.Type.ReloadedAll, null!, null); } - /// Whether an Undo for the given design is possible. - public bool CanUndo(Design? design) - => design != null && _undoStore.ContainsKey(design.Identifier); - /// Create a new temporary design without adding it to the manager. public DesignBase CreateTemporary() - => new(_customizations, _items); + => new(Customizations, Items); /// Create a new design of a given name. public Design CreateEmpty(string name, bool handlePath) { var (actualName, path) = ParseName(name, handlePath); - var design = new Design(_customizations, _items) + var design = new Design(Customizations, Items) { CreationDate = DateTimeOffset.UtcNow, LastEdit = DateTimeOffset.UtcNow, Identifier = CreateNewGuid(), Name = actualName, - Index = _designs.Count, + Index = Designs.Count, }; - _designs.Add(design); + Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, path); return design; } @@ -132,13 +121,13 @@ public class DesignManager LastEdit = DateTimeOffset.UtcNow, Identifier = CreateNewGuid(), Name = actualName, - Index = _designs.Count, + Index = Designs.Count, }; - _designs.Add(design); + Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, path); return design; } @@ -152,26 +141,30 @@ public class DesignManager LastEdit = DateTimeOffset.UtcNow, Identifier = CreateNewGuid(), Name = actualName, - Index = _designs.Count, + Index = Designs.Count, }; - _designs.Add(design); + Designs.Add(design); Glamourer.Log.Debug( $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, path); return design; } /// Delete a design. public void Delete(Design design) { - foreach (var d in _designs.Skip(design.Index + 1)) + foreach (var d in Designs.Skip(design.Index + 1)) --d.Index; - _designs.RemoveAt(design.Index); - _saveService.ImmediateDelete(design); - _event.Invoke(DesignChanged.Type.Deleted, design, null); + Designs.RemoveAt(design.Index); + SaveService.ImmediateDelete(design); + DesignChanged.Invoke(DesignChanged.Type.Deleted, design, null); } + #endregion + + #region Edit Information + /// Rename a design. public void Rename(Design design, string newName) { @@ -181,9 +174,9 @@ public class DesignManager design.Name = newName; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.Renamed, design, oldName); + DesignChanged.Invoke(DesignChanged.Type.Renamed, design, oldName); } /// Change the description of a design. @@ -195,9 +188,9 @@ public class DesignManager design.Description = description; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); + DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); } public void ChangeColor(Design design, string newColor) @@ -208,9 +201,9 @@ public class DesignManager design.Color = newColor; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.ChangedColor, design, oldColor); + DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, oldColor); } /// Add a new tag to a design. The tags remain sorted. @@ -222,15 +215,11 @@ public class DesignManager design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); design.LastEdit = DateTimeOffset.UtcNow; var idx = design.Tags.IndexOf(tag); - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); + DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); } - /// Remove a tag from a design if it exists. - public void RemoveTag(Design design, string tag) - => RemoveTag(design, design.Tags.IndexOf(tag)); - /// Remove a tag from a design by its index. public void RemoveTag(Design design, int tagIdx) { @@ -240,9 +229,9 @@ public class DesignManager var oldTag = design.Tags[tagIdx]; design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); } /// Rename a tag from a design by its index. The tags stay sorted. @@ -255,9 +244,9 @@ public class DesignManager design.Tags[tagIdx] = newTag; Array.Sort(design.Tags); design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); - _event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); } /// Add an associated mod to a design. @@ -267,9 +256,9 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); } /// Remove an associated mod from a design. @@ -279,9 +268,9 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); } /// Set the write protection status of a design. @@ -290,56 +279,14 @@ public class DesignManager if (!design.SetWriteProtected(value)) return; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); - _event.Invoke(DesignChanged.Type.WriteProtection, design, value); + DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value); } - /// Change a customization value. - public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) - { - var oldValue = design.DesignData.Customize[idx]; + #endregion - switch (idx) - { - case CustomizeIndex.Race: - case CustomizeIndex.BodyType: - Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); - return; - case CustomizeIndex.Clan: - { - var customize = design.DesignData.Customize; - if (_customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0) - return; - if (!design.SetCustomize(_customizations, customize)) - return; - - break; - } - case CustomizeIndex.Gender: - { - var customize = design.DesignData.Customize; - if (_customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0) - return; - if (!design.SetCustomize(_customizations, customize)) - return; - - break; - } - default: - if (!_customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender, - design.DesignData.Customize.Face, idx, value) - || !design.GetDesignDataRef().Customize.Set(idx, value)) - return; - - break; - } - - design.LastEdit = DateTimeOffset.UtcNow; - Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); - _saveService.QueueSave(design); - _event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); - } + #region Edit Application Rules /// Change whether to apply a specific customize value. public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) @@ -348,115 +295,21 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); - _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); - } - - /// Change a non-weapon equipment piece. - public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) - { - if (!_items.IsItemValid(slot, item.Id, out item)) - return; - - var old = design.DesignData.Item(slot); - if (!design.GetDesignDataRef().SetItem(slot, item)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - Glamourer.Log.Debug( - $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); - _saveService.QueueSave(design); - _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); - } - - /// Change a weapon. - public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) - { - var currentMain = design.DesignData.Item(EquipSlot.MainHand); - var currentOff = design.DesignData.Item(EquipSlot.OffHand); - switch (slot) - { - case EquipSlot.MainHand: - var newOff = currentOff; - if (!_items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item)) - return; - - if (item.Type != currentMain.Type) - { - var defaultOffhand = _items.GetDefaultOffhand(item); - if (!_items.IsOffhandValid(item, defaultOffhand.ItemId, out newOff)) - return; - } - - if (!(design.GetDesignDataRef().SetItem(EquipSlot.MainHand, item) - | design.GetDesignDataRef().SetItem(EquipSlot.OffHand, newOff))) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug( - $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); - _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); - - return; - case EquipSlot.OffHand: - if (!_items.IsOffhandValid(currentOff.Type, item.ItemId, out item)) - return; - - if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug( - $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); - _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); - return; - default: return; - } - } - - /// Change a customize parameter. - public void ChangeCustomizeParameter(Design design, CustomizeParameterFlag flag, CustomizeParameterValue value) - { - var old = design.DesignData.Parameters[flag]; - if (!design.GetDesignDataRef().Parameters.Set(flag, value)) - return; - - var @new = design.DesignData.Parameters[flag]; - design.LastEdit = DateTimeOffset.UtcNow; - Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); - _saveService.QueueSave(design); - _event.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag)); + DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); } /// Change whether to apply a specific equipment piece. - public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + public void ChangeApplyItem(Design design, EquipSlot slot, bool value) { if (!design.SetApplyEquip(slot, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); - } - - /// Change the stain for any equipment piece. - public void ChangeStain(Design design, EquipSlot slot, StainId stain) - { - if (_items.ValidateStain(stain, out _, false).Length > 0) - return; - - var oldStain = design.DesignData.Stain(slot); - if (!design.GetDesignDataRef().SetStain(slot, stain)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}."); - _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, slot); } /// Change whether to apply a specific stain. @@ -466,22 +319,9 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); - } - - /// Change the crest visibility for any equipment piece. - public void ChangeCrest(Design design, CrestFlag slot, bool crest) - { - var oldCrest = design.DesignData.Crest(slot); - if (!design.GetDesignDataRef().SetCrest(slot, crest)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); - _event.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); + DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, slot); } /// Change whether to apply a specific crest visibility. @@ -491,49 +331,21 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + 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) - { - var change = metaIndex switch - { - ActorState.MetaIndex.Wetness => design.GetDesignDataRef().SetIsWet(value), - ActorState.MetaIndex.HatState => design.GetDesignDataRef().SetHatVisible(value), - ActorState.MetaIndex.VisorState => design.GetDesignDataRef().SetVisor(value), - ActorState.MetaIndex.WeaponState => design.GetDesignDataRef().SetWeaponVisible(value), - _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), - }; - if (!change) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); - _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, slot); } /// Change the application value of one of the meta flags. - public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value) + public void ChangeApplyMeta(Design design, MetaIndex metaIndex, bool value) { - var change = metaIndex switch - { - ActorState.MetaIndex.Wetness => design.SetApplyWetness(value), - ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value), - ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value), - ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value), - _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), - }; - if (!change) + if (!design.SetApplyMeta(metaIndex, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); - _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); } /// Change the application value of a customize parameter. @@ -543,83 +355,35 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}."); - _event.Invoke(DesignChanged.Type.ApplyParameter, design, flag); + DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, flag); } - /// Apply an entire design based on its appliance rules piece by piece. - public void ApplyDesign(Design design, DesignBase other) - { - _undoStore[design.Identifier] = design.DesignData; - if (other.DoApplyWetness()) - design.GetDesignDataRef().SetIsWet(other.DesignData.IsWet()); - if (other.DoApplyHatVisible()) - design.GetDesignDataRef().SetHatVisible(other.DesignData.IsHatVisible()); - if (other.DoApplyVisorToggle()) - design.GetDesignDataRef().SetVisor(other.DesignData.IsVisorToggled()); - if (other.DoApplyWeaponVisible()) - design.GetDesignDataRef().SetWeaponVisible(other.DesignData.IsWeaponVisible()); - - if (design.DesignData.IsHuman) - { - foreach (var index in Enum.GetValues()) - { - if (other.DoApplyCustomize(index)) - ChangeCustomize(design, index, other.DesignData.Customize[index]); - } - - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - if (other.DoApplyEquip(slot)) - ChangeEquip(design, slot, other.DesignData.Item(slot)); - - if (other.DoApplyStain(slot)) - ChangeStain(design, slot, other.DesignData.Stain(slot)); - } - - foreach (var slot in Enum.GetValues()) - { - if (other.DoApplyCrest(slot)) - ChangeCrest(design, slot, other.DesignData.Crest(slot)); - } - } - - if (other.DoApplyEquip(EquipSlot.MainHand)) - ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand)); - - if (other.DoApplyEquip(EquipSlot.OffHand)) - ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand)); - - if (other.DoApplyStain(EquipSlot.MainHand)) - ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand)); - - if (other.DoApplyStain(EquipSlot.OffHand)) - ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand)); - } + #endregion public void UndoDesignChange(Design design) { - if (!_undoStore.Remove(design.Identifier, out var otherData)) + if (!UndoStore.Remove(design.Identifier, out var otherData)) return; var other = CreateTemporary(); - other.SetDesignData(_customizations, otherData); + other.SetDesignData(Customizations, otherData); ApplyDesign(design, other); } private void MigrateOldDesigns() { - if (!File.Exists(_saveService.FileNames.MigrationDesignFile)) + if (!File.Exists(SaveService.FileNames.MigrationDesignFile)) return; var errors = 0; var skips = 0; var successes = 0; - var oldDesigns = _designs.ToList(); + var oldDesigns = Designs.ToList(); try { - var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile); + var text = File.ReadAllText(SaveService.FileNames.MigrationDesignFile); var dict = JsonConvert.DeserializeObject>(text) ?? new Dictionary(); var migratedFileSystemPaths = new Dictionary(dict.Count); foreach (var (name, base64) in dict) @@ -627,14 +391,14 @@ public class DesignManager try { var actualName = Path.GetFileName(name); - var design = new Design(_customizations, _items) + var design = new Design(Customizations, Items) { - CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile), - LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile), + CreationDate = File.GetCreationTimeUtc(SaveService.FileNames.MigrationDesignFile), + LastEdit = File.GetLastWriteTimeUtc(SaveService.FileNames.MigrationDesignFile), Identifier = CreateNewGuid(), Name = actualName, }; - design.MigrateBase64(_customizations, _items, _humans, base64); + design.MigrateBase64(Customizations, Items, _humans, base64); if (!oldDesigns.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate)) { Add(design, $"Migrated old design to {design.Identifier}."); @@ -655,24 +419,24 @@ public class DesignManager } } - DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths); + DesignFileSystem.MigrateOldPaths(SaveService, migratedFileSystemPaths); Glamourer.Log.Information( $"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs."); } catch (Exception e) { - Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}"); + Glamourer.Log.Error($"Could not migrate old design file {SaveService.FileNames.MigrationDesignFile}:\n{e}"); } try { - File.Move(_saveService.FileNames.MigrationDesignFile, - Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak")); - Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file."); + File.Move(SaveService.FileNames.MigrationDesignFile, + Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak")); + Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file."); } catch (Exception ex) { - Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}"); + Glamourer.Log.Error($"Could not move migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file:\n{ex}"); } } @@ -702,7 +466,7 @@ public class DesignManager { try { - var correctName = _saveService.FileNames.DesignFile(design); + var correctName = SaveService.FileNames.DesignFile(design); File.Move(name, correctName, false); Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}."); } @@ -722,7 +486,7 @@ public class DesignManager while (true) { var guid = Guid.NewGuid(); - if (_designs.All(d => d.Identifier != guid)) + if (!Designs.Contains(guid)) return guid; } } @@ -732,18 +496,17 @@ public class DesignManager /// Returns false if the design is already contained or if the identifier is already in use. /// The design is treated as newly created and invokes an event. /// - private bool Add(Design design, string? message) + private void Add(Design design, string? message) { - if (_designs.Any(d => d == design || d.Identifier == design.Identifier)) - return false; + if (Designs.Any(d => d == design || d.Identifier == design.Identifier)) + return; - design.Index = _designs.Count; - _designs.Add(design); + design.Index = Designs.Count; + Designs.Add(design); if (!message.IsNullOrEmpty()) Glamourer.Log.Debug(message); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, null); - return true; + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, null); } /// Split a given string into its folder path and its name, if is true. diff --git a/Glamourer/Designs/DesignStorage.cs b/Glamourer/Designs/DesignStorage.cs new file mode 100644 index 0000000..a87415c --- /dev/null +++ b/Glamourer/Designs/DesignStorage.cs @@ -0,0 +1,18 @@ +using OtterGui.Services; + +namespace Glamourer.Designs; + +public class DesignStorage : List, IService +{ + public bool TryGetValue(Guid identifier, [NotNullWhen(true)] out Design? design) + { + design = ByIdentifier(identifier); + return design != null; + } + + public Design? ByIdentifier(Guid identifier) + => this.FirstOrDefault(d => d.Identifier == identifier); + + public bool Contains(Guid identifier) + => ByIdentifier(identifier) != null; +} diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs new file mode 100644 index 0000000..b655327 --- /dev/null +++ b/Glamourer/Designs/IDesignEditor.cs @@ -0,0 +1,71 @@ +using Glamourer.Designs.Links; +using Glamourer.GameData; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public readonly record struct ApplySettings( + uint Key = 0, + StateSource Source = StateSource.Manual, + bool RespectManual = false, + bool FromJobChange = false, + bool UseSingleSource = false, + bool MergeLinks = false) +{ + public static readonly ApplySettings Manual = new() + { + Key = 0, + Source = StateSource.Manual, + FromJobChange = false, + RespectManual = false, + UseSingleSource = false, + MergeLinks = false, + }; + + public static readonly ApplySettings Game = new() + { + Key = 0, + Source = StateSource.Game, + FromJobChange = false, + RespectManual = false, + UseSingleSource = false, + MergeLinks = false, + }; +} + +public interface IDesignEditor +{ + /// Change a customization value. + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings = default); + + /// Change an entire customize array according to the given flags. + public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings = default); + + /// Change a customize parameter. + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue v, ApplySettings settings = default); + + /// Change an equipment piece. + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) + => ChangeEquip(data, slot, item, null, settings); + + /// Change the stain for any equipment piece. + public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings = default) + => ChangeEquip(data, slot, null, stain, settings); + + /// Change an equipment piece and its stain at the same time. + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings settings = default); + + /// Change the crest visibility for any equipment piece. + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default); + + /// Change the bool value of one of the meta flags. + public void ChangeMetaState(object data, MetaIndex slot, bool value, ApplySettings settings = default); + + /// Change all values applies from the given design. + public void ApplyDesign(object data, MergedDesign design, ApplySettings settings = default); + + /// Change all values applies from the given design. + public void ApplyDesign(object data, DesignBase design, ApplySettings settings = default); +} diff --git a/Glamourer/Designs/Links/DesignLink.cs b/Glamourer/Designs/Links/DesignLink.cs new file mode 100644 index 0000000..a9fb805 --- /dev/null +++ b/Glamourer/Designs/Links/DesignLink.cs @@ -0,0 +1,19 @@ +using Glamourer.Automation; + +namespace Glamourer.Designs.Links; + +public record struct DesignLink(Design Link, ApplicationType Type); + +public readonly record struct LinkData(Guid Identity, ApplicationType Type, LinkOrder Order) +{ + public override string ToString() + => Identity.ToString(); +} + +public enum LinkOrder : byte +{ + Self, + After, + Before, + None, +}; diff --git a/Glamourer/Designs/Links/DesignLinkLoader.cs b/Glamourer/Designs/Links/DesignLinkLoader.cs new file mode 100644 index 0000000..4d438bd --- /dev/null +++ b/Glamourer/Designs/Links/DesignLinkLoader.cs @@ -0,0 +1,27 @@ +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Services; + +namespace Glamourer.Designs.Links; + +public sealed class DesignLinkLoader(DesignStorage designStorage, MessageService messager) + : DelayedReferenceLoader(messager), IService +{ + protected override bool TryGetObject(LinkData data, [NotNullWhen(true)] out Design? obj) + => designStorage.FindFirst(d => d.Identifier == data.Identity, out obj); + + protected override bool SetObject(Design parent, Design child, LinkData data, out string error) + => LinkContainer.AddLink(parent, child, data.Type, data.Order, out error); + + protected override void HandleChildNotFound(Design parent, LinkData data) + { + Messager.AddMessage(new Notification( + $"Could not find the design {data.Identity}. If this design was deleted, please re-save {parent.Identifier}.", + NotificationType.Warning)); + } + + protected override void HandleChildNotSet(Design parent, Design child, string error) + => Messager.AddMessage(new Notification($"Could not link {child.Identifier} to {parent.Identifier}: {error}", + NotificationType.Warning)); +} diff --git a/Glamourer/Designs/Links/DesignLinkManager.cs b/Glamourer/Designs/Links/DesignLinkManager.cs new file mode 100644 index 0000000..76d9c9a --- /dev/null +++ b/Glamourer/Designs/Links/DesignLinkManager.cs @@ -0,0 +1,85 @@ +using Glamourer.Automation; +using Glamourer.Events; +using Glamourer.Services; +using OtterGui.Services; + +namespace Glamourer.Designs.Links; + +public sealed class DesignLinkManager : IService, IDisposable +{ + private readonly DesignStorage _storage; + private readonly DesignChanged _event; + private readonly SaveService _saveService; + + public DesignLinkManager(DesignStorage storage, DesignChanged @event, SaveService saveService) + { + _storage = storage; + _event = @event; + _saveService = saveService; + + _event.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignLinkManager); + } + + public void Dispose() + => _event.Unsubscribe(OnDesignChanged); + + public void MoveDesignLink(Design parent, int idxFrom, LinkOrder orderFrom, int idxTo, LinkOrder orderTo) + { + if (!parent.Links.Reorder(idxFrom, orderFrom, idxTo, orderTo)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Moved link from {orderFrom} {idxFrom} to {idxTo} {orderTo}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void AddDesignLink(Design parent, Design child, LinkOrder order) + { + if (!LinkContainer.AddLink(parent, child, ApplicationType.All, order, out _)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Added new {order} link to {child.Identifier} for {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void RemoveDesignLink(Design parent, int idx, LinkOrder order) + { + if (!parent.Links.Remove(idx, order)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Removed the {order} link at {idx} for {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void ChangeApplicationType(Design parent, int idx, LinkOrder order, ApplicationType applicationType) + { + applicationType &= ApplicationType.All; + if (!parent.Links.ChangeApplicationRules(idx, order, applicationType, out var old)) + return; + + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Changed link application type from {old} to {applicationType} for design link {order} {idx + 1} in design {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, object? _) + { + if (type is not DesignChanged.Type.Deleted) + return; + + foreach (var design in _storage) + { + if (!design.Links.Remove(deletedDesign)) + continue; + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Removed {deletedDesign.Identifier} from {design.Identifier} links due to deletion."); + _saveService.QueueSave(design); + } + } +} diff --git a/Glamourer/Designs/Links/DesignMerger.cs b/Glamourer/Designs/Links/DesignMerger.cs new file mode 100644 index 0000000..438c0f4 --- /dev/null +++ b/Glamourer/Designs/Links/DesignMerger.cs @@ -0,0 +1,272 @@ +using Glamourer.Automation; +using Glamourer.GameData; +using Glamourer.Services; +using Glamourer.State; +using Glamourer.Unlocks; +using OtterGui.Services; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs.Links; + +public class DesignMerger( + DesignManager designManager, + CustomizeService _customize, + Configuration _config, + ItemUnlockManager _itemUnlocks, + CustomizeUnlockManager _customizeUnlocks) : IService +{ + public MergedDesign Merge(LinkContainer designs, in DesignData baseRef, bool respectOwnership, bool modAssociations) + => Merge(designs.Select(d => ((DesignBase?) d.Link, d.Type)), baseRef, respectOwnership, modAssociations); + + public MergedDesign Merge(IEnumerable<(DesignBase?, ApplicationType)> designs, in DesignData baseRef, bool respectOwnership, + bool modAssociations) + { + var ret = new MergedDesign(designManager); + CustomizeFlag fixFlags = 0; + respectOwnership &= _config.UnlockedItemMode; + foreach (var (design, type) in designs) + { + if (type is 0) + continue; + + ref readonly var data = ref design == null ? ref baseRef : ref design.GetDesignDataRef(); + var source = design == null ? StateSource.Game : StateSource.Manual; + + if (!data.IsHuman) + continue; + + var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyMeta) = type.ApplyWhat(design); + ReduceMeta(data, applyMeta, ret, source); + ReduceCustomize(data, customizeFlags, ref fixFlags, ret, source, respectOwnership); + ReduceEquip(data, equipFlags, ret, source, respectOwnership); + ReduceMainhands(data, equipFlags, ret, source, respectOwnership); + ReduceOffhands(data, equipFlags, ret, source, respectOwnership); + ReduceCrests(data, crestFlags, ret, source); + ReduceParameters(data, parameterFlags, ret, source); + ReduceMods(design as Design, ret, modAssociations); + } + + ApplyFixFlags(ret, fixFlags); + return ret; + } + + + private static void ReduceMods(Design? design, MergedDesign ret, bool modAssociations) + { + if (design == null || !modAssociations) + return; + + foreach (var (mod, settings) in design.AssociatedMods) + ret.AssociatedMods.TryAdd(mod, settings); + } + + private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source) + { + applyMeta &= ~ret.Design.ApplyMeta; + + foreach (var index in MetaExtensions.AllRelevant) + { + if (!applyMeta.HasFlag(index.ToFlag())) + continue; + + ret.Design.SetApplyMeta(index, true); + ret.Design.GetDesignDataRef().SetMeta(index, design.GetMeta(index)); + ret.Sources[index] = source; + } + } + + private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source) + { + crestFlags &= ~ret.Design.ApplyCrest; + if (crestFlags == 0) + return; + + foreach (var slot in CrestExtensions.AllRelevantSet) + { + if (!crestFlags.HasFlag(slot)) + continue; + + ret.Design.GetDesignDataRef().SetCrest(slot, design.Crest(slot)); + ret.Design.SetApplyCrest(slot, true); + ret.Sources[slot] = source; + } + } + + private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret, + StateSource source) + { + parameterFlags &= ~ret.Design.ApplyParameters; + if (parameterFlags == 0) + return; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + if (!parameterFlags.HasFlag(flag)) + continue; + + ret.Design.GetDesignDataRef().Parameters.Set(flag, design.Parameters[flag]); + ret.Design.SetApplyParameter(flag, true); + ret.Sources[flag] = source; + } + } + + private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, + bool respectOwnership) + { + equipFlags &= ~ret.Design.ApplyEquip; + if (equipFlags == 0) + return; + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var flag = slot.ToFlag(); + + if (equipFlags.HasFlag(flag)) + { + var item = design.Item(slot); + if (!respectOwnership || _itemUnlocks.IsUnlocked(item.Id, out _)) + ret.Design.GetDesignDataRef().SetItem(slot, item); + ret.Design.SetApplyEquip(slot, true); + ret.Sources[slot, false] = source; + } + + var stainFlag = slot.ToStainFlag(); + if (equipFlags.HasFlag(stainFlag)) + { + ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot)); + ret.Design.SetApplyStain(slot, true); + ret.Sources[slot, true] = source; + } + } + + foreach (var slot in EquipSlotExtensions.WeaponSlots) + { + var stainFlag = slot.ToStainFlag(); + if (equipFlags.HasFlag(stainFlag)) + { + ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot)); + ret.Design.SetApplyStain(slot, true); + ret.Sources[slot, true] = source; + } + } + } + + private void ReduceMainhands(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, + bool respectOwnership) + { + if (!equipFlags.HasFlag(EquipFlag.Mainhand)) + return; + + var weapon = design.Item(EquipSlot.MainHand); + if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _)) + return; + + if (!ret.Design.DoApplyEquip(EquipSlot.MainHand)) + { + ret.Design.SetApplyEquip(EquipSlot.MainHand, true); + ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon); + } + + ret.Weapons.TryAdd(weapon.Type, (weapon, source)); + } + + private void ReduceOffhands(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, bool respectOwnership) + { + if (!equipFlags.HasFlag(EquipFlag.Offhand)) + return; + + var weapon = design.Item(EquipSlot.OffHand); + if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _)) + return; + + if (!ret.Design.DoApplyEquip(EquipSlot.OffHand)) + { + ret.Design.SetApplyEquip(EquipSlot.OffHand, true); + ret.Design.GetDesignDataRef().SetItem(EquipSlot.OffHand, weapon); + } + + if (weapon.Valid) + ret.Weapons.TryAdd(weapon.Type, (weapon, source)); + } + + private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret, + StateSource source, bool respectOwnership) + { + customizeFlags &= ~ret.Design.ApplyCustomizeRaw; + if (customizeFlags == 0) + return; + + // Skip anything not human. + if (!ret.Design.DesignData.IsHuman || !design.IsHuman) + return; + + var customize = ret.Design.DesignData.Customize; + if (customizeFlags.HasFlag(CustomizeFlag.Clan)) + { + fixFlags |= _customize.ChangeClan(ref customize, design.Customize.Clan); + ret.Design.SetApplyCustomize(CustomizeIndex.Clan, true); + ret.Design.SetApplyCustomize(CustomizeIndex.Race, true); + customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race); + ret.Sources[CustomizeIndex.Clan] = source; + ret.Sources[CustomizeIndex.Race] = source; + } + + if (customizeFlags.HasFlag(CustomizeFlag.Gender)) + { + fixFlags |= _customize.ChangeGender(ref customize, design.Customize.Gender); + ret.Design.SetApplyCustomize(CustomizeIndex.Gender, true); + customizeFlags &= ~CustomizeFlag.Gender; + ret.Sources[CustomizeIndex.Gender] = source; + } + + if (customizeFlags.HasFlag(CustomizeFlag.Face)) + { + customize[CustomizeIndex.Face] = design.Customize.Face; + ret.Design.SetApplyCustomize(CustomizeIndex.Face, true); + customizeFlags &= ~CustomizeFlag.Face; + ret.Sources[CustomizeIndex.Face] = source; + } + + var set = _customize.Manager.GetSet(customize.Clan, customize.Gender); + var face = customize.Face; + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!customizeFlags.HasFlag(flag)) + continue; + + var value = design.Customize[index]; + if (!CustomizeService.IsCustomizationValid(set, face, index, value, out var data)) + continue; + + if (data.HasValue && respectOwnership && !_customizeUnlocks.IsUnlocked(data.Value, out _)) + continue; + + customize[index] = data?.Value ?? value; + ret.Design.SetApplyCustomize(index, true); + ret.Sources[index] = source; + fixFlags &= ~flag; + } + + ret.Design.SetCustomize(_customize, customize); + } + + private static void ApplyFixFlags(MergedDesign ret, CustomizeFlag fixFlags) + { + if (fixFlags == 0) + return; + + var source = ret.Design.DoApplyCustomize(CustomizeIndex.Clan) + ? ret.Sources[CustomizeIndex.Clan] + : ret.Sources[CustomizeIndex.Gender]; + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!fixFlags.HasFlag(flag)) + continue; + + ret.Sources[index] = source; + ret.Design.SetApplyCustomize(index, true); + } + } +} diff --git a/Glamourer/Designs/Links/LinkContainer.cs b/Glamourer/Designs/Links/LinkContainer.cs new file mode 100644 index 0000000..ef67688 --- /dev/null +++ b/Glamourer/Designs/Links/LinkContainer.cs @@ -0,0 +1,193 @@ +using Glamourer.Automation; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; + +namespace Glamourer.Designs.Links; + +public sealed class LinkContainer : List +{ + public List Before + => this; + + public readonly List After = []; + + public new int Count + => base.Count + After.Count; + + public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder) + { + var fromList = fromOrder switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + + var toList = toOrder switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + + if (fromList == toList) + return fromList.Move(fromIndex, toIndex); + + if (fromIndex < 0 || fromIndex >= fromList.Count) + return false; + + toIndex = Math.Clamp(toIndex, 0, toList.Count); + toList.Insert(toIndex, fromList[fromIndex]); + fromList.RemoveAt(fromIndex); + return true; + } + + public bool Remove(int idx, LinkOrder order) + { + var list = order switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + if (idx < 0 || idx >= list.Count) + return false; + + list.RemoveAt(idx); + return true; + } + + public bool ChangeApplicationRules(int idx, LinkOrder order, ApplicationType type, out ApplicationType old) + { + var list = order switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + old = list[idx].Type; + if (idx < 0 || idx >= list.Count || old == type) + return false; + + list[idx] = list[idx] with { Type = type }; + return true; + } + + public static bool CanAddLink(Design parent, Design child, LinkOrder order, out string error) + { + if (parent == child) + { + error = $"Can not link {parent.Incognito} with itself."; + return false; + } + + if (parent.Links.Contains(child)) + { + error = $"Design {parent.Incognito} already contains a direct link to {child.Incognito}."; + return false; + } + + if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order)) + { + error = $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction."; + return false; + } + + if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order)) + { + error = $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction."; + return false; + } + + error = string.Empty; + return true; + } + + public static bool AddLink(Design parent, Design child, ApplicationType type, LinkOrder order, out string error) + { + if (!CanAddLink(parent, child, order, out error)) + return false; + + var list = order switch + { + LinkOrder.Before => parent.Links.Before, + LinkOrder.After => parent.Links.After, + _ => null, + }; + + if (list == null) + { + error = $"Order {order} is invalid."; + return false; + } + + type &= ApplicationType.All; + list.Add(new DesignLink(child, type)); + error = string.Empty; + return true; + } + + public bool Contains(Design child) + => Before.Any(l => l.Link == child) || After.Any(l => l.Link == child); + + public bool Remove(Design child) + => Before.RemoveAll(l => l.Link == child) + After.RemoveAll(l => l.Link == child) > 0; + + public static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(Design design) + { + var set = new HashSet(design.Links.Count * 4); + return GetAllLinks(new DesignLink(design, ApplicationType.All), LinkOrder.Self, set); + } + + private static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(DesignLink design, LinkOrder currentOrder, ISet visited) + { + if (design.Link.Links.Count == 0) + { + if (visited.Add(design.Link)) + yield return (design, currentOrder); + + yield break; + } + + foreach (var link in design.Link.Links.Before + .Where(l => !visited.Contains(l.Link)) + .SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.After ? LinkOrder.After : LinkOrder.Before, visited))) + yield return link; + + if (visited.Add(design.Link)) + yield return (design, currentOrder); + + foreach (var link in design.Link.Links.After.Where(l => !visited.Contains(l.Link)) + .SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.Before ? LinkOrder.Before : LinkOrder.After, visited))) + yield return link; + } + + public JObject Serialize() + { + var before = new JArray(); + foreach (var link in Before) + { + before.Add(new JObject + { + ["Design"] = link.Link.Identifier, + ["Type"] = (uint)link.Type, + }); + } + + var after = new JArray(); + foreach (var link in After) + { + after.Add(new JObject + { + ["Design"] = link.Link.Identifier, + ["Type"] = (uint)link.Type, + }); + } + + return new JObject + { + [nameof(Before)] = before, + [nameof(After)] = after, + }; + } +} diff --git a/Glamourer/Designs/Links/MergedDesign.cs b/Glamourer/Designs/Links/MergedDesign.cs new file mode 100644 index 0000000..bcfaa39 --- /dev/null +++ b/Glamourer/Designs/Links/MergedDesign.cs @@ -0,0 +1,51 @@ +using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Links; + +public sealed class MergedDesign +{ + public MergedDesign(DesignManager designManager) + { + Design = designManager.CreateTemporary(); + Design.ApplyEquip = 0; + Design.ApplyCustomize = 0; + Design.ApplyCrest = 0; + Design.ApplyParameters = 0; + Design.ApplyMeta = 0; + } + + public MergedDesign(DesignBase design) + { + Design = design; + if (design.DoApplyEquip(EquipSlot.MainHand)) + { + var weapon = design.DesignData.Item(EquipSlot.MainHand); + if (weapon.Valid) + Weapons.TryAdd(weapon.Type, (weapon, StateSource.Manual)); + } + + if (design.DoApplyEquip(EquipSlot.OffHand)) + { + var weapon = design.DesignData.Item(EquipSlot.OffHand); + if (weapon.Valid) + Weapons.TryAdd(weapon.Type, (weapon, StateSource.Manual)); + } + } + + public MergedDesign(Design design) + : this((DesignBase)design) + { + foreach (var (mod, settings) in design.AssociatedMods) + AssociatedMods[mod] = settings; + } + + public readonly DesignBase Design; + public readonly Dictionary Weapons = new(4); + public readonly SortedList AssociatedMods = []; + public StateSources Sources = new(); +} diff --git a/Glamourer/Designs/MetaIndex.cs b/Glamourer/Designs/MetaIndex.cs new file mode 100644 index 0000000..edbf7b6 --- /dev/null +++ b/Glamourer/Designs/MetaIndex.cs @@ -0,0 +1,69 @@ +using Glamourer.State; + +namespace Glamourer.Designs; + +public enum MetaIndex +{ + Wetness = StateIndex.MetaWetness, + HatState = StateIndex.MetaHatState, + VisorState = StateIndex.MetaVisorState, + WeaponState = StateIndex.MetaWeaponState, + ModelId = StateIndex.MetaModelId, +} + +[Flags] +public enum MetaFlag : byte +{ + Wetness = 0x01, + HatState = 0x02, + VisorState = 0x04, + WeaponState = 0x08, +} + +public static class MetaExtensions +{ + public static readonly IReadOnlyList AllRelevant = + [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState]; + + public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; + + public static MetaFlag ToFlag(this MetaIndex index) + => index switch + { + MetaIndex.Wetness => MetaFlag.Wetness, + MetaIndex.HatState => MetaFlag.HatState, + MetaIndex.VisorState => MetaFlag.VisorState, + MetaIndex.WeaponState => MetaFlag.WeaponState, + _ => (MetaFlag)byte.MaxValue, + }; + + public static MetaIndex ToIndex(this MetaFlag index) + => index switch + { + MetaFlag.Wetness => MetaIndex.Wetness, + MetaFlag.HatState => MetaIndex.HatState, + MetaFlag.VisorState => MetaIndex.VisorState, + MetaFlag.WeaponState => MetaIndex.WeaponState, + _ => (MetaIndex)byte.MaxValue, + }; + + public static string ToName(this MetaIndex index) + => index switch + { + MetaIndex.HatState => "Hat Visible", + MetaIndex.VisorState => "Visor Toggled", + MetaIndex.WeaponState => "Weapon Visible", + MetaIndex.Wetness => "Force Wetness", + _ => "Unknown Meta", + }; + + public static string ToTooltip(this MetaIndex index) + => index switch + { + MetaIndex.HatState => "Hide or show the characters head gear.", + MetaIndex.VisorState => "Toggle the visor state of the characters head gear.", + MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.", + MetaIndex.Wetness => "Force the character to be wet or not.", + _ => string.Empty, + }; +} diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 2217c34..f3ebb5f 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -12,7 +12,7 @@ namespace Glamourer.Events; /// Parameter is any additional data depending on the type of change. /// /// -public sealed class DesignChanged() +public sealed class DesignChanged() : EventWrapper(nameof(DesignChanged)) { public enum Type @@ -50,13 +50,19 @@ public sealed class DesignChanged() /// An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. RemovedMod, + /// An existing design had a link to a different design added, removed or moved. Data is null. + ChangedLink, + /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. Customize, + /// An existing design had its entire customize array changed. Data is the old array, the applied flags and the changed flags. [(CustomizeArray, CustomizeFlag, CustomizeFlag)]. + EntireCustomize, + /// An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. Equip, - /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. + /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand, the new offhand (if any) and the new gauntlets (if any). [(EquipItem, EquipItem, EquipItem, EquipItem?, EquipItem?)]. Weapon, /// An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. @@ -92,6 +98,9 @@ public sealed class DesignChanged() public enum Priority { + /// + DesignLinkManager = 1, + /// AutoDesignManager = 1, diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 01e2758..47b5d20 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -2,68 +2,59 @@ using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Classes; -namespace Glamourer.Events; - -/// -/// Triggered when a Design is edited in any way. -/// -/// Parameter is the type of the change -/// Parameter is the changed saved state. -/// Parameter is the existing actors using this saved state. -/// Parameter is any additional data depending on the type of change. -/// -/// -public sealed class StateChanged() - : EventWrapper(nameof(StateChanged)) +namespace Glamourer.Events { - public enum Type + /// + /// Triggered when a Design is edited in any way. + /// + /// Parameter is the type of the change + /// Parameter is the changed saved state. + /// Parameter is the existing actors using this saved state. + /// Parameter is any additional data depending on the type of change. + /// + /// + public sealed class StateChanged() + : EventWrapper(nameof(StateChanged)) { - /// A characters saved state had the model id changed. This means everything may have changed. Data is the old model id and the new model id. [(uint, uint)] - Model, + public enum Type + { + /// A characters saved state had the model id changed. This means everything may have changed. Data is the old model id and the new model id. [(uint, uint)] + Model, - /// A characters saved state had multiple customization values changed. TData is the old customize array and the applied changes. [(Customize, CustomizeFlag)] - EntireCustomize, + /// A characters saved state had multiple customization values changed. TData is the old customize array and the applied changes. [(Customize, CustomizeFlag)] + EntireCustomize, - /// A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. - Customize, + /// A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + Customize, - /// A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. - Equip, + /// A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + Equip, - /// A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. - Weapon, + /// A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. + Weapon, - /// 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 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 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 its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. - Parameter, + /// A characters saved state had its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. + Parameter, - /// A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] - Design, + /// A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] + Design, - /// A characters saved state had its state reset to its game values. This means everything may have changed. Data is null. - Reset, + /// A characters saved state had its state reset to its game values. This means everything may have changed. Data is null. + Reset, - /// A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. - Other, - } + /// A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. + Other, + } - public enum Source : byte - { - Game, - Manual, - Fixed, - Ipc, - // Only used for CustomizeParameters. - Pending, - } - - public enum Priority - { - GlamourerIpc = int.MinValue, + public enum Priority + { + GlamourerIpc = int.MinValue, + } } } diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs index 0bae022..aa43b79 100644 --- a/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs @@ -1,19 +1,28 @@ using Glamourer.Designs; -using Glamourer.Events; using Glamourer.GameData; using Glamourer.State; namespace Glamourer.Gui.Customization; -public ref struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data) +public struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data) { + private IDesignEditor _editor; + private object _object; public readonly CustomizeParameterFlag Flag = flag; public bool Locked; public bool DisplayApplication; public bool AllowRevert; - public Action ValueSetter = null!; - public Action ApplySetter = null!; + public readonly void ChangeParameter(CustomizeParameterValue value) + => _editor.ChangeCustomizeParameter(_object, Flag, value, ApplySettings.Manual); + + public readonly void ChangeApplyParameter(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyParameter(design, Flag, value); + } + public CustomizeParameterValue CurrentValue = data.Parameters[flag]; public CustomizeParameterValue GameValue; public bool CurrentApply; @@ -21,19 +30,20 @@ public ref struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in Des public static CustomizeParameterDrawData FromDesign(DesignManager manager, Design design, CustomizeParameterFlag flag) => new(flag, design.DesignData) { + _editor = manager, + _object = design, Locked = design.WriteProtected(), DisplayApplication = true, CurrentApply = design.DoApplyParameter(flag), - ValueSetter = v => manager.ChangeCustomizeParameter(design, flag, v), - ApplySetter = v => manager.ChangeApplyParameter(design, flag, v), }; public static CustomizeParameterDrawData FromState(StateManager manager, ActorState state, CustomizeParameterFlag flag) => new(flag, state.ModelData) { + _editor = manager, + _object = state, Locked = state.IsLocked, DisplayApplication = false, - ValueSetter = v => manager.ChangeCustomizeParameter(state, flag, v, StateChanged.Source.Manual), GameValue = state.BaseData.Parameters[flag], AllowRevert = true, }; diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs index 414398e..9bfb2f8 100644 --- a/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs @@ -195,7 +195,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import using (_ = ImRaii.Disabled(data.Locked || noHighlights)) { if (ImGui.ColorEdit3("##value", ref value, GetFlags())) - data.ValueSetter(new CustomizeParameterValue(value)); + data.ChangeParameter(new CustomizeParameterValue(value)); } if (noHighlights) @@ -215,7 +215,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import using (_ = ImRaii.Disabled(data.Locked)) { if (ImGui.ColorEdit4("##value", ref value, GetFlags() | ImGuiColorEditFlags.AlphaPreviewHalf)) - data.ValueSetter(new CustomizeParameterValue(value)); + data.ChangeParameter(new CustomizeParameterValue(value)); } DrawRevert(data); @@ -231,7 +231,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import using (_ = ImRaii.Disabled(data.Locked)) { if (ImGui.InputFloat("##value", ref value, 0.1f, 0.5f)) - data.ValueSetter(new CustomizeParameterValue(value)); + data.ChangeParameter(new CustomizeParameterValue(value)); } DrawRevert(data); @@ -247,7 +247,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import using (_ = ImRaii.Disabled(data.Locked)) { if (ImGui.SliderFloat("##value", ref value, -100f, 300, "%.2f")) - data.ValueSetter(new CustomizeParameterValue(value / 100f)); + data.ChangeParameter(new CustomizeParameterValue(value / 100f)); ImGuiUtil.HoverTooltip("You can control-click this to enter arbitrary values by hand instead of dragging."); } @@ -262,7 +262,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import return; if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) - data.ValueSetter(data.GameValue); + data.ChangeParameter(data.GameValue); ImGuiUtil.HoverTooltip("Hold Control and Right-click to revert to game values."); } @@ -271,7 +271,7 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import { if (UiHelpers.DrawCheckbox("##apply", "Apply this custom parameter when applying the Design.", data.CurrentApply, out var enabled, data.Locked)) - data.ApplySetter(enabled); + data.ChangeApplyParameter(enabled); } private void DrawApplyAndLabel(in CustomizeParameterDrawData data) @@ -310,6 +310,6 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), new Vector2(ImGui.GetFrameHeight()), _copy.HasValue ? "Paste the currently copied value." : "No value copied yet.", locked || !_copy.HasValue, true)) - data.ValueSetter(_copy!.Value); + data.ChangeParameter(_copy!.Value); } } diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index f6e73f9..78e813e 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -5,7 +5,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Glamourer.Automation; -using Glamourer.Events; +using Glamourer.Designs; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.State; @@ -32,7 +32,7 @@ public sealed class DesignQuickBar : Window, IDisposable private readonly ImRaii.Style _windowPadding = new(); private readonly ImRaii.Color _windowColor = new(); private DateTime _keyboardToggle = DateTime.UnixEpoch; - private int _numButtons = 0; + private int _numButtons; public DesignQuickBar(Configuration config, DesignCombo designCombo, StateManager stateManager, IKeyState keyState, ObjectManager objects, AutoDesignApplier autoDesignApplier) @@ -163,7 +163,7 @@ public sealed class DesignQuickBar : Window, IDisposable var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, design, ApplySettings.Manual); } public void DrawRevertButton(Vector2 buttonSize) @@ -189,7 +189,7 @@ public sealed class DesignQuickBar : Window, IDisposable var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); if (clicked) - _stateManager.ResetState(state!, StateChanged.Source.Manual); + _stateManager.ResetState(state!, StateSource.Manual); } public void DrawRevertAutomationButton(Vector2 buttonSize) @@ -257,7 +257,7 @@ public sealed class DesignQuickBar : Window, IDisposable ImGui.SameLine(); var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, tooltip, available); if (clicked) - _stateManager.ResetAdvancedState(state!, StateChanged.Source.Manual); + _stateManager.ResetAdvancedState(state!, StateSource.Manual); } private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, string tooltip, diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs index 57da890..67c6a9e 100644 --- a/Glamourer/Gui/Equipment/EquipDrawData.cs +++ b/Glamourer/Gui/Equipment/EquipDrawData.cs @@ -1,29 +1,45 @@ -using Dalamud.Game.Inventory; -using Glamourer.Designs; -using Glamourer.Events; +using Glamourer.Designs; using Glamourer.State; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Gui.Equipment; -public ref struct EquipDrawData(EquipSlot slot, in DesignData designData) +public struct EquipDrawData(EquipSlot slot, in DesignData designData) { - public readonly EquipSlot Slot = slot; - public bool Locked; - public bool DisplayApplication; - public bool AllowRevert; + private IDesignEditor _editor; + private object _object; + public readonly EquipSlot Slot = slot; + public bool Locked; + public bool DisplayApplication; + public bool AllowRevert; - public Action ItemSetter = null!; - public Action StainSetter = null!; - public Action ApplySetter = null!; - public Action ApplyStainSetter = null!; - public EquipItem CurrentItem = designData.Item(slot); - public StainId CurrentStain = designData.Stain(slot); - public EquipItem GameItem = default; - public StainId GameStain = default; - public bool CurrentApply; - public bool CurrentApplyStain; + public readonly void SetItem(EquipItem item) + => _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual); + + public readonly void SetStain(StainId stain) + => _editor.ChangeStain(_object, Slot, stain, ApplySettings.Manual); + + public readonly void SetApplyItem(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyItem(design, Slot, value); + } + + public readonly void SetApplyStain(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyStain(design, Slot, value); + } + + public EquipItem CurrentItem = designData.Item(slot); + public StainId CurrentStain = designData.Stain(slot); + public EquipItem GameItem = default; + public StainId GameStain = default; + public bool CurrentApply; + public bool CurrentApplyStain; public readonly Gender CurrentGender = designData.Customize.Gender; public readonly Race CurrentRace = designData.Customize.Race; @@ -31,12 +47,8 @@ public ref struct EquipDrawData(EquipSlot slot, in DesignData designData) public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot) => new(slot, design.DesignData) { - ItemSetter = slot.IsEquipment() || slot.IsAccessory() - ? i => manager.ChangeEquip(design, slot, i) - : i => manager.ChangeWeapon(design, slot, i), - StainSetter = i => manager.ChangeStain(design, slot, i), - ApplySetter = b => manager.ChangeApplyEquip(design, slot, b), - ApplyStainSetter = b => manager.ChangeApplyStain(design, slot, b), + _editor = manager, + _object = design, CurrentApply = design.DoApplyEquip(slot), CurrentApplyStain = design.DoApplyStain(slot), Locked = design.WriteProtected(), @@ -46,8 +58,8 @@ public ref struct EquipDrawData(EquipSlot slot, in DesignData designData) public static EquipDrawData FromState(StateManager manager, ActorState state, EquipSlot slot) => new(slot, state.ModelData) { - ItemSetter = i => manager.ChangeItem(state, slot, i, StateChanged.Source.Manual), - StainSetter = i => manager.ChangeStain(state, slot, i, StateChanged.Source.Manual), + _editor = manager, + _object = state, Locked = state.IsLocked, DisplayApplication = false, GameItem = state.BaseData.Item(slot), diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index 22f40d7..8dfa50c 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -205,7 +205,7 @@ public class EquipmentDrawer { var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); if (newSetId.Id != current.CurrentItem.PrimaryId.Id) - current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant)); + current.SetItem(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant)); } ImGui.SameLine(); @@ -214,7 +214,7 @@ public class EquipmentDrawer { var newType = (SecondaryId)Math.Clamp(type, 0, ushort.MaxValue); if (newType.Id != current.CurrentItem.SecondaryId.Id) - current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant)); + current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant)); } ImGui.SameLine(); @@ -223,7 +223,7 @@ public class EquipmentDrawer { var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); if (newVariant.Id != current.CurrentItem.Variant.Id) - current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId, + current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId, newVariant)); } } @@ -239,7 +239,7 @@ public class EquipmentDrawer var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); if (newStainId != data.CurrentStain.Id) - data.StainSetter(newStainId); + data.SetStain(newStainId); } /// Draw an input for armor that can set arbitrary values instead of choosing items. @@ -252,7 +252,7 @@ public class EquipmentDrawer { var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); if (newSetId.Id != data.CurrentItem.PrimaryId.Id) - data.ItemSetter(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant)); + data.SetItem(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant)); } ImGui.SameLine(); @@ -261,7 +261,7 @@ public class EquipmentDrawer { var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); if (newVariant != data.CurrentItem.Variant) - data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant)); + data.SetItem(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant)); } } @@ -365,7 +365,7 @@ public class EquipmentDrawer mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (ImRaii.Group()) { DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left); if (mainhand.DisplayApplication) @@ -391,7 +391,7 @@ public class EquipmentDrawer var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (ImRaii.Group()) { DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left); if (offhand.DisplayApplication) @@ -420,12 +420,12 @@ public class EquipmentDrawer : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); if (change) if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) - data.StainSetter(stain.RowIndex); + data.SetStain(stain.RowIndex); else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - data.StainSetter(Stain.None.RowIndex); + data.SetStain(Stain.None.RowIndex); - if (ResetOrClear(data.Locked, false, data.AllowRevert, true, data.CurrentStain, data.GameStain, Stain.None.RowIndex, out var id)) - data.StainSetter(Stain.None.RowIndex); + if (ResetOrClear(data.Locked, false, data.AllowRevert, true, data.CurrentStain, data.GameStain, Stain.None.RowIndex, out _)) + data.SetStain(Stain.None.RowIndex); } private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open) @@ -441,13 +441,13 @@ public class EquipmentDrawer var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); if (change) - data.ItemSetter(combo.CurrentSelection); + data.SetItem(combo.CurrentSelection); else if (combo.CustomVariant.Id > 0) - data.ItemSetter(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); + data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot), out var item)) - data.ItemSetter(item); + data.SetItem(item); } private static bool ResetOrClear(bool locked, bool clicked, bool allowRevert, bool allowClear, @@ -467,9 +467,9 @@ public class EquipmentDrawer (true, true, true) => ("Right-click to clear. Control and Right-Click to revert to game.", revertItem, true), (true, true, false) => ("Right-click to clear. Control and Right-Click to revert to game.", clearItem, true), (true, false, true) => ("Control and Right-Click to revert to game.", revertItem, true), - (true, false, false) => ("Control and Right-Click to revert to game.", (T?)default, false), + (true, false, false) => ("Control and Right-Click to revert to game.", default, false), (false, true, _) => ("Right-click to clear.", clearItem, true), - (false, false, _) => (string.Empty, (T?)default, false), + (false, false, _) => (string.Empty, default, false), }; ImGuiUtil.HoverTooltip(tt); @@ -502,11 +502,11 @@ public class EquipmentDrawer if (changedItem != null) { - mainhand.ItemSetter(changedItem.Value); + mainhand.SetItem(changedItem.Value); if (changedItem.Value.Type.ValidOffhand() != mainhand.CurrentItem.Type.ValidOffhand()) { offhand.CurrentItem = _items.GetDefaultOffhand(changedItem.Value); - offhand.ItemSetter(offhand.CurrentItem); + offhand.SetItem(offhand.CurrentItem); } mainhand.CurrentItem = changedItem.Value; @@ -533,18 +533,18 @@ public class EquipmentDrawer UiHelpers.OpenCombo($"##{combo.Label}"); if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) - offhand.ItemSetter(combo.CurrentSelection); + offhand.SetItem(combo.CurrentSelection); var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem); - if (ResetOrClear(locked, open, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item)) - offhand.ItemSetter(item); + if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item)) + offhand.SetItem(item); } private static void DrawApply(in EquipDrawData data) { if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this item when applying the Design.", data.CurrentApply, out var enabled, data.Locked)) - data.ApplySetter(enabled); + data.SetApplyItem(enabled); } private static void DrawApplyStain(in EquipDrawData data) @@ -552,7 +552,7 @@ public class EquipmentDrawer if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this item when applying the Design.", data.CurrentApplyStain, out var enabled, data.Locked)) - data.ApplyStainSetter(enabled); + data.SetApplyStain(enabled); } #endregion diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index d030abb..4cb11d0 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -1,4 +1,4 @@ -using Glamourer.Events; +using Glamourer.Designs; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; @@ -11,7 +11,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui; -public class PenumbraChangedItemTooltip : IDisposable +public sealed class PenumbraChangedItemTooltip : IDisposable { private readonly PenumbraService _penumbra; private readonly StateManager _stateManager; @@ -111,24 +111,24 @@ public class PenumbraChangedItemTooltip : IDisposable switch (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) { case (false, false): - Glamourer.Log.Information($"Applying {item.Name} to Right Finger."); + Glamourer.Log.Debug($"Applying {item.Name} to Right Finger."); SetLastItem(EquipSlot.RFinger, item, state); - _stateManager.ChangeItem(state, EquipSlot.RFinger, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.RFinger, item, ApplySettings.Manual); break; case (false, true): - Glamourer.Log.Information($"Applying {item.Name} to Left Finger."); + Glamourer.Log.Debug($"Applying {item.Name} to Left Finger."); SetLastItem(EquipSlot.LFinger, item, state); - _stateManager.ChangeItem(state, EquipSlot.LFinger, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.LFinger, item, ApplySettings.Manual); break; case (true, false) when last.Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Right Finger."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to Right Finger."); SetLastItem(EquipSlot.RFinger, default, state); - _stateManager.ChangeItem(state, EquipSlot.RFinger, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.RFinger, last, ApplySettings.Manual); break; case (true, true) when _lastItems[EquipSlot.LFinger.ToIndex()].Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Left Finger."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to Left Finger."); SetLastItem(EquipSlot.LFinger, default, state); - _stateManager.ChangeItem(state, EquipSlot.LFinger, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.LFinger, last, ApplySettings.Manual); break; } @@ -136,15 +136,15 @@ public class PenumbraChangedItemTooltip : IDisposable default: if (ImGui.GetIO().KeyCtrl && last.Valid) { - Glamourer.Log.Information($"Re-Applying {last.Name} to {slot.ToName()}."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to {slot.ToName()}."); SetLastItem(slot, default, state); - _stateManager.ChangeItem(state, slot, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, slot, last, ApplySettings.Manual); } else { - Glamourer.Log.Information($"Applying {item.Name} to {slot.ToName()}."); + Glamourer.Log.Debug($"Applying {item.Name} to {slot.ToName()}."); SetLastItem(slot, item, state); - _stateManager.ChangeItem(state, slot, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, slot, item, ApplySettings.Manual); } return; diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 431eed0..bef2ea5 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -5,7 +5,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Glamourer.Automation; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; @@ -61,13 +60,13 @@ public class ActorPanel( if (_importService.CreateDatTarget(out var dat)) { - _stateManager.ChangeCustomize(_state!, dat.Customize, CustomizeApplicationFlags, StateChanged.Source.Manual); + _stateManager.ChangeEntireCustomize(_state!, dat.Customize, CustomizeApplicationFlags, ApplySettings.Manual); Glamourer.Messager.NotificationMessage($"Applied games .dat file {dat.Description} customizations to {_state.Identifier}.", NotificationType.Success, false); } else if (_importService.CreateCharaTarget(out var designBase, out var name)) { - _stateManager.ApplyDesign(designBase, _state!, StateChanged.Source.Manual); + _stateManager.ApplyDesign(_state!, designBase, ApplySettings.Manual); Glamourer.Messager.NotificationMessage($"Applied Anamnesis .chara file {name} to {_state.Identifier}.", NotificationType.Success, false); } @@ -139,9 +138,9 @@ public class ActorPanel( return; if (_customizationDrawer.Draw(_state!.ModelData.Customize, _state.IsLocked, _lockedRedraw)) - _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); + _stateManager.ChangeEntireCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, ApplySettings.Manual); - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.Wetness, _stateManager, _state)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.Wetness, _stateManager, _state)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } @@ -159,7 +158,7 @@ public class ActorPanel( var data = EquipDrawData.FromState(_stateManager, _state!, slot); _equipmentDrawer.DrawEquip(data); if (usedAllStain) - _stateManager.ChangeStain(_state, slot, newAllStain, StateChanged.Source.Manual); + _stateManager.ChangeStain(_state, slot, newAllStain, ApplySettings.Manual); } var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand); @@ -187,21 +186,21 @@ public class ActorPanel( { using (_ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.HatState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!)); } ImGui.SameLine(); using (_ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.VisorState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!)); } ImGui.SameLine(); using (_ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.WeaponState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!)); } } @@ -264,7 +263,7 @@ public class ActorPanel( } if (turnHuman) - _stateManager.TurnHuman(_state, StateChanged.Source.Manual); + _stateManager.TurnHuman(_state, StateSource.Manual); } private HeaderDrawer.Button SetFromClipboardButton() @@ -316,9 +315,9 @@ public class ActorPanel( private void SaveDesignOpen() { ImGui.OpenPopup("Save as Design"); - _newName = _state!.Identifier.ToName(); + _newName = _state!.Identifier.ToName(); var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest, applyParameters); + _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest, applyParameters); } private void SaveDesignDrawPopup() @@ -340,7 +339,7 @@ public class ActorPanel( var text = ImGui.GetClipboardText(); var design = _converter.FromBase64(text, applyCustomize, applyGear, out _) ?? throw new Exception("The clipboard did not contain valid data."); - _stateManager.ApplyDesign(design, _state!, StateChanged.Source.Manual); + _stateManager.ApplyDesign(_state!, design, ApplySettings.Manual with { MergeLinks = true }); } catch (Exception ex) { @@ -368,7 +367,7 @@ public class ActorPanel( { if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.", _state!.IsLocked)) - _stateManager.ResetState(_state!, StateChanged.Source.Manual); + _stateManager.ResetState(_state!, StateSource.Manual); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Reapply State", Vector2.Zero, "Try to reapply the configured state if something went wrong.", @@ -395,8 +394,8 @@ public class ActorPanel( var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), state, - StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), + ApplySettings.Manual); } private void DrawApplyToTarget() @@ -413,7 +412,7 @@ public class ActorPanel( var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), state, - StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), + ApplySettings.Manual); } } diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index a50445e..0387d58 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -264,7 +264,7 @@ public class SetPanel( var size = new Vector2(ImGui.GetFrameHeight()); size.X += ImGuiHelpers.GlobalScale; - var (equipFlags, customizeFlags, _, _, _, _, _, _) = design.ApplyWhat(); + var (equipFlags, customizeFlags, _, _, _) = design.ApplyWhat(); var sb = new StringBuilder(); foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { @@ -369,13 +369,13 @@ public class SetPanel( private void DrawApplicationTypeBoxes(AutoDesignSet set, AutoDesign design, int autoDesignIndex, bool singleLine) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale)); - var newType = design.ApplicationType; + var newType = design.Type; var newTypeInt = (uint)newType; style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using (_ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value())) { - if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)AutoDesign.Type.All)) - newType = (AutoDesign.Type)newTypeInt; + if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)ApplicationType.All)) + newType = (ApplicationType)newTypeInt; } style.Pop(); @@ -384,8 +384,8 @@ public class SetPanel( { void Box(int idx) { - var (type, description) = Types[idx]; - var value = design.ApplicationType.HasFlag(type); + var (type, description) = ApplicationTypeExtensions.Types[idx]; + var value = design.Type.HasFlag(type); if (ImGui.Checkbox($"##{(byte)type}", ref value)) newType = value ? newType | type : newType & ~type; ImGuiUtil.HoverTooltip(description); @@ -428,40 +428,20 @@ public class SetPanel( _manager.ChangeIdentifier(setIndex, _identifierDrawer.MannequinIdentifier); } - - private static readonly IReadOnlyList<(AutoDesign.Type, string)> Types = new[] + private sealed class JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) + : FilterComboCache(() => jobs.JobGroups.Values.ToList(), log) { - (AutoDesign.Type.Customizations, - "Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."), - (AutoDesign.Type.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."), - (AutoDesign.Type.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."), - (AutoDesign.Type.GearCustomization, "Apply all dye and crest changes that are enabled in this design."), - (AutoDesign.Type.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), - }; - - private sealed class JobGroupCombo : FilterComboCache - { - private readonly AutoDesignManager _manager; - private readonly JobService _jobs; - - public JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) - : base(() => jobs.JobGroups.Values.ToList(), log) - { - _manager = manager; - _jobs = jobs; - } - public void Draw(AutoDesignSet set, AutoDesign design, int autoDesignIndex) { CurrentSelection = design.Jobs; - CurrentSelectionIdx = _jobs.JobGroups.Values.IndexOf(j => j.Id == design.Jobs.Id); + CurrentSelectionIdx = jobs.JobGroups.Values.IndexOf(j => j.Id == design.Jobs.Id); if (Draw("##JobGroups", design.Jobs.Name, "Select for which job groups this design should be applied.\nControl + Right-Click to set to all classes.", ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelectionIdx >= 0) - _manager.ChangeJobCondition(set, autoDesignIndex, CurrentSelection); + manager.ChangeJobCondition(set, autoDesignIndex, CurrentSelection); else if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) - _manager.ChangeJobCondition(set, autoDesignIndex, _jobs.JobGroups[1]); + manager.ChangeJobCondition(set, autoDesignIndex, jobs.JobGroups[1]); } protected override string ToString(JobGroup obj) diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs index 5408a7c..7b3f594 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs @@ -52,11 +52,11 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM ImGuiUtil.DrawTableColumn(state.Identifier.ToString()); ImGui.TableNextColumn(); if (ImGui.Button("Reset")) - stateManager.ResetState(state, StateChanged.Source.Manual); + stateManager.ResetState(state, StateSource.Manual); ImGui.TableNextRow(); - static void PrintRow(string label, T actor, T model, StateChanged.Source source) where T : notnull + static void PrintRow(string label, T actor, T model, StateSource source) where T : notnull { ImGuiUtil.DrawTableColumn(label); ImGuiUtil.DrawTableColumn(actor.ToString()!); @@ -70,44 +70,44 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM return $"{item.Name} ({item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; } - PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]); + PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state.Sources[MetaIndex.ModelId]); ImGui.TableNextRow(); - PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaIndex.Wetness]); + PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state.Sources[MetaIndex.Wetness]); ImGui.TableNextRow(); if (state.BaseData.IsHuman && state.ModelData.IsHuman) { - PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaIndex.HatState]); + PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state.Sources[MetaIndex.HatState]); ImGui.TableNextRow(); PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), - state[ActorState.MetaIndex.VisorState]); + state.Sources[MetaIndex.VisorState]); ImGui.TableNextRow(); PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), - state[ActorState.MetaIndex.WeaponState]); + state.Sources[MetaIndex.WeaponState]); 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.Sources[slot, false]); 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.Sources[slot, true].ToString()); } foreach (var type in Enum.GetValues()) { - PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]); + PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state.Sources[type]); ImGui.TableNextRow(); } foreach (var crest in CrestExtensions.AllRelevantSet) { - PrintRow(crest.ToLabel(), state.BaseData.Crest(crest), state.ModelData.Crest(crest), state[crest]); + PrintRow(crest.ToLabel(), state.BaseData.Crest(crest), state.ModelData.Crest(crest), state.Sources[crest]); ImGui.TableNextRow(); } foreach (var flag in CustomizeParameterExtensions.AllFlags) { - PrintRow(flag.ToString(), state.BaseData.Parameters[flag], state.ModelData.Parameters[flag], state[flag]); + PrintRow(flag.ToString(), state.BaseData.Parameters[flag], state.ModelData.Parameters[flag], state.Sources[flag]); ImGui.TableNextRow(); } } diff --git a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs index cbc0e40..6dd7f6e 100644 --- a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs @@ -42,7 +42,7 @@ public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IGameDataDr foreach (var (design, designIdx) in set.Designs.WithIndex()) { ImGuiUtil.DrawTableColumn($"{design.Name(false)} ({designIdx})"); - ImGuiUtil.DrawTableColumn($"{design.ApplicationType} {design.Jobs.Name}"); + ImGuiUtil.DrawTableColumn($"{design.Type} {design.Jobs.Name}"); } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs index d4070ef..85b4010 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs @@ -25,9 +25,8 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ continue; DrawDesign(design, _designFileSystem); - var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw, - design.DoApplyHatVisible(), - design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected()); + var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw, design.ApplyMeta, + design.WriteProtected()); using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImGuiUtil.TextWrapped(base64); if (ImGui.IsItemClicked()) @@ -85,18 +84,12 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ ImGuiUtil.DrawTableColumn(applyCrest ? "Apply" : "Keep"); } - ImGuiUtil.DrawTableColumn("Hat Visible"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsHatVisible().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyHatVisible() ? "Apply" : "Keep"); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Visor Toggled"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsVisorToggled().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyVisorToggle() ? "Apply" : "Keep"); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Weapon Visible"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsWeaponVisible().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyWeaponVisible() ? "Apply" : "Keep"); - ImGui.TableNextRow(); + foreach (var index in MetaExtensions.AllRelevant) + { + ImGuiUtil.DrawTableColumn(index.ToName()); + ImGuiUtil.DrawTableColumn(design.DesignData.GetMeta(index).ToString()); + ImGuiUtil.DrawTableColumn(design.DoApplyMeta(index) ? "Apply" : "Keep"); + } ImGuiUtil.DrawTableColumn("Model ID"); ImGuiUtil.DrawTableColumn(design.DesignData.ModelId.ToString()); @@ -111,9 +104,5 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); ImGui.TableNextRow(); } - - ImGuiUtil.DrawTableColumn("Is Wet"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsWet().ToString()); - ImGui.TableNextRow(); } } diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs index 7bc83f9..c893f4c 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs @@ -55,10 +55,8 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IGa try { - _parse64 = DesignBase64Migration.MigrateBase64(_items, _humans, _base64, out var ef, out var cf, out var wp, out var ah, - out var av, - out var aw); - _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, aw, wp); + _parse64 = DesignBase64Migration.MigrateBase64(_items, _humans, _base64, out var ef, out var cf, out var wp, out var meta); + _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, meta, wp); _restoreBytes = Convert.FromBase64String(_restore); } catch (Exception ex) diff --git a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs index 505ed62..04537b5 100644 --- a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs @@ -2,7 +2,6 @@ using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.GameData; using Glamourer.Interop; using Glamourer.State; @@ -67,9 +66,9 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled)) { foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true)) - _state.ChangeEquip(state!, slot, item, stain, StateChanged.Source.Manual); - _state.ChangeVisorState(state!, data.VisorToggled, StateChanged.Source.Manual); - _state.ChangeCustomize(state!, data.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Manual); + _state.ChangeEquip(state!, slot, item, stain, ApplySettings.Manual); + _state.ChangeMetaState(state!, MetaIndex.VisorState, data.VisorToggled, ApplySettings.Manual); + _state.ChangeEntireCustomize(state!, data.Customize, CustomizeFlagExtensions.All, ApplySettings.Manual); } ImGui.TableNextColumn(); diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index a4f71c0..cb401a1 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -128,7 +128,11 @@ public sealed class DesignFileSystemSelector : FileSystemSelector= _dragDropTargetIndex; --i) + _linkManager.MoveDesignLink(_selector.Selected!, i, LinkOrder.Before, 0, LinkOrder.After); + break; + case LinkOrder.After: + for (var i = 0; i <= _dragDropTargetIndex; ++i) + { + _linkManager.MoveDesignLink(_selector.Selected!, 0, LinkOrder.After, _selector.Selected!.Links.Before.Count, + LinkOrder.Before); + } + + break; + } + else if (_dragDropTargetOrder is LinkOrder.Self) + _linkManager.MoveDesignLink(_selector.Selected!, _dragDropIndex, _dragDropOrder, _selector.Selected!.Links.Before.Count, + LinkOrder.Before); + else + _linkManager.MoveDesignLink(_selector.Selected!, _dragDropIndex, _dragDropOrder, _dragDropTargetIndex, _dragDropTargetOrder); + + _dragDropIndex = -1; + _dragDropTargetIndex = -1; + _dragDropOrder = LinkOrder.None; + _dragDropTargetOrder = LinkOrder.None; + } + + private void DrawList() + { + using var table = ImRaii.Table("table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter); + if (!table) + return; + + ImGui.TableSetupColumn("Del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Detail", ImGuiTableColumnFlags.WidthFixed, + 6 * ImGui.GetFrameHeight() + 5 * ImGui.GetStyle().ItemInnerSpacing.X); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + DrawSubList(_selector.Selected!.Links.Before, LinkOrder.Before); + DrawSelf(); + DrawSubList(_selector.Selected!.Links.After, LinkOrder.After); + DrawNew(); + MoveLink(); + } + + private void DrawSelf() + { + using var id = ImRaii.PushId((int)LinkOrder.Self); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(_selector.IncognitoMode ? _selector.Selected!.Incognito : _selector.Selected!.Name.Text); + DrawDragDrop(_selector.Selected!, LinkOrder.Self, 0); + ImGui.TableNextColumn(); + } + + private void DrawSubList(IReadOnlyList list, LinkOrder order) + { + using var id = ImRaii.PushId((int)order); + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + for (var i = 0; i < list.Count; ++i) + { + id.Push(i); + + ImGui.TableNextColumn(); + var delete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this link.", false, true); + var (design, flags) = list[i]; + ImGui.TableNextColumn(); + + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(_selector.IncognitoMode ? design.Incognito : design.Name.Text); + DrawDragDrop(design, order, i); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + DrawApplicationBoxes(i, order, flags); + + if (delete) + _linkManager.RemoveDesignLink(_selector.Selected!, i--, order); + } + } + + private void DrawNew() + { + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + _combo.Draw(ImGui.GetContentRegionAvail().X); + ImGui.TableNextColumn(); + string ttBefore, ttAfter; + bool canAddBefore, canAddAfter; + if (_combo.Design == null) + { + ttAfter = ttBefore = "Select a design first."; + canAddBefore = canAddAfter = false; + } + else + { + canAddBefore = LinkContainer.CanAddLink(_selector.Selected!, _combo.Design, LinkOrder.Before, out var error); + ttBefore = canAddBefore + ? $"Add a link at the top of the list to {_combo.Design.Name}." + : $"Can not add a link to {_combo.Design.Name}:\n{error}"; + canAddAfter = LinkContainer.CanAddLink(_selector.Selected!, _combo.Design, LinkOrder.After, out error); + ttAfter = canAddAfter + ? $"Add a link at the bottom of the list to {_combo.Design.Name}." + : $"Can not add a link to {_combo.Design.Name}:\n{error}"; + } + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowCircleUp.ToIconString(), buttonSize, ttBefore, !canAddBefore, true)) + { + _linkManager.AddDesignLink(_selector.Selected!, _combo.Design!, LinkOrder.Before); + _linkManager.MoveDesignLink(_selector.Selected!, _selector.Selected!.Links.Before.Count - 1, LinkOrder.Before, 0, LinkOrder.Before); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowCircleDown.ToIconString(), buttonSize, ttAfter, !canAddAfter, true)) + _linkManager.AddDesignLink(_selector.Selected!, _combo.Design!, LinkOrder.After); + } + + private void DrawDragDrop(Design design, LinkOrder order, int index) + { + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("DraggingLink", IntPtr.Zero, 0); + ImGui.TextUnformatted($"Reordering {design.Name}..."); + _dragDropIndex = index; + _dragDropOrder = order; + } + } + + using var target = ImRaii.DragDropTarget(); + if (!target) + return; + + if (!ImGuiUtil.IsDropping("DraggingLink")) + return; + + _dragDropTargetIndex = index; + _dragDropTargetOrder = order; + } + + private void DrawApplicationBoxes(int idx, LinkOrder order, ApplicationType current) + { + var newType = current; + var newTypeInt = (uint)newType; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale)) + { + using var _ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); + if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)ApplicationType.All)) + newType = (ApplicationType)newTypeInt; + } + + ImGuiUtil.HoverTooltip("Toggle all application modes at once."); + + ImGui.SameLine(); + Box(0); + ImGui.SameLine(); + Box(1); + ImGui.SameLine(); + + Box(2); + ImGui.SameLine(); + Box(3); + ImGui.SameLine(); + Box(4); + if (newType != current) + _linkManager.ChangeApplicationType(_selector.Selected!, idx, order, current); + return; + + void Box(int i) + { + var (applicationType, description) = ApplicationTypeExtensions.Types[i]; + var value = applicationType.HasFlag(applicationType); + if (ImGui.Checkbox($"##{(byte)applicationType}", ref value)) + newType = value ? newType | applicationType : newType & ~applicationType; + ImGuiUtil.HoverTooltip(description); + } + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 447fc5d..93ebad5 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -4,7 +4,6 @@ using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.System.Framework; using Glamourer.Automation; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.GameData; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; @@ -31,7 +30,8 @@ public class DesignPanel( DesignConverter _converter, ImportService _importService, MultiDesignPanel _multiDesignPanel, - CustomizeParameterDrawer _parameterDrawer) + CustomizeParameterDrawer _parameterDrawer, + DesignLinkDrawer _designLinkDrawer) { private readonly FileDialogManager _fileDialog = new(); @@ -119,21 +119,21 @@ public class DesignPanel( { using (var _ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.HatState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.HatState, _manager, _selector.Selected!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Head, _manager, _selector.Selected!)); } ImGui.SameLine(); using (var _ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.VisorState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.VisorState, _manager, _selector.Selected!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Body, _manager, _selector.Selected!)); } ImGui.SameLine(); using (var _ = ImRaii.Group()) { - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.WeaponState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.WeaponState, _manager, _selector.Selected!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.OffHand, _manager, _selector.Selected!)); } } @@ -158,7 +158,7 @@ public class DesignPanel( _manager.ChangeCustomize(_selector.Selected, idx, _customizationDrawer.Customize[idx]); } - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.Wetness, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.Wetness, _manager, _selector.Selected!)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } @@ -166,6 +166,7 @@ public class DesignPanel( { if (!_config.UseAdvancedParameters) return; + using var h = ImRaii.CollapsingHeader("Advanced Customizations"); if (!h) return; @@ -255,24 +256,24 @@ public class DesignPanel( { 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); + _manager.ChangeApplyItem(_selector.Selected!, slot, apply); } } - ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[] + ApplyEquip("Weapons", ApplicationTypeExtensions.WeaponFlags, false, new[] { EquipSlot.MainHand, EquipSlot.OffHand, }); ImGui.NewLine(); - ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); + ApplyEquip("Armor", ApplicationTypeExtensions.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); ImGui.NewLine(); - ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); + ApplyEquip("Accessories", ApplicationTypeExtensions.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); ImGui.NewLine(); - ApplyEquip("Dyes", AutoDesign.StainFlags, true, + ApplyEquip("Dyes", ApplicationTypeExtensions.StainFlags, true, EquipSlotExtensions.FullSlots); ImGui.NewLine(); @@ -285,28 +286,25 @@ public class DesignPanel( private void DrawMetaApplication() { - using var id = ImRaii.PushId("Meta"); - const uint all = 0x0Fu; - var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00) - | (_selector.Selected!.DoApplyVisorToggle() ? 0x02u : 0x00) - | (_selector.Selected!.DoApplyWeaponVisible() ? 0x04u : 0x00) - | (_selector.Selected!.DoApplyWetness() ? 0x08u : 0x00); - var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); - var apply = bigChange ? (flags & 0x01) == 0x01 : _selector.Selected!.DoApplyHatVisible(); - if (ImGui.Checkbox("Apply Hat Visibility", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.HatState, apply); + using var id = ImRaii.PushId("Meta"); + const uint all = (uint)MetaExtensions.All; + var flags = (uint)_selector.Selected!.ApplyMeta; + var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); - apply = bigChange ? (flags & 0x02) == 0x02 : _selector.Selected!.DoApplyVisorToggle(); - if (ImGui.Checkbox("Apply Visor State", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.VisorState, apply); + var labels = new[] + { + "Apply Hat Visibility", + "Apply Visor State", + "Apply Weapon Visibility", + "Apply Wetness", + }; - apply = bigChange ? (flags & 0x04) == 0x04 : _selector.Selected!.DoApplyWeaponVisible(); - if (ImGui.Checkbox("Apply Weapon Visibility", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.WeaponState, apply); - - apply = bigChange ? (flags & 0x08) == 0x08 : _selector.Selected!.DoApplyWetness(); - if (ImGui.Checkbox("Apply Wetness", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.Wetness, apply); + foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(labels)) + { + var apply = bigChange ? ((MetaFlag)flags).HasFlag(index.ToFlag()) : _selector.Selected!.DoApplyMeta(index); + if (ImGui.Checkbox(label, ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, index, apply); + } } private void DrawParameterApplication() @@ -370,6 +368,7 @@ public class DesignPanel( _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); + _designLinkDrawer.Draw(); } private void DrawButtonRow() @@ -439,7 +438,7 @@ public class DesignPanel( { var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.Manual with { MergeLinks = true }); } } @@ -458,7 +457,7 @@ public class DesignPanel( { var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.Manual with {MergeLinks = true}); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index 2852128..bd04a57 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -2,14 +2,12 @@ using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Interop; using Glamourer.State; using ImGuiNET; -using Lumina.Data.Parsing.Scd; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -172,7 +170,7 @@ public class NpcPanel( _equipDrawer.DrawWeapons(mainhandData, offhandData, false); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(ActorState.MetaIndex.VisorState, _selector.Selection.VisorToggled)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(MetaIndex.VisorState, _selector.Selection.VisorToggled)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } @@ -202,7 +200,7 @@ public class NpcPanel( { var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags(); var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0); - _state.ApplyDesign(design, state, StateChanged.Source.Manual); + _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -219,9 +217,9 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest, applyParameters); - _state.ApplyDesign(design, state, StateChanged.Source.Manual); + var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags(); + var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0); + _state.ApplyDesign(state, design, ApplySettings.Manual); } } diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index b883dd9..e8435ce 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -68,6 +68,9 @@ public class SettingsTab( if (!ImGui.CollapsingHeader("Glamourer Behavior")) return; + Checkbox("Always Apply Entire Weapon for Mainhand", + "When manually applying a mainhand item, will also apply a corresponding offhand and potentially gauntlets for certain fist weapons.", + config.ChangeEntireItem, v => config.ChangeEntireItem = v); Checkbox("Use Replacement Gear for Gear Unavailable to Your Race or Gender", "Use different gender- and race-appropriate models as a substitute when detecting certain items not available for a characters current gender and race.", config.UseRestrictedGearProtection, v => config.UseRestrictedGearProtection = v); diff --git a/Glamourer/Gui/ToggleDrawData.cs b/Glamourer/Gui/ToggleDrawData.cs index ed435c4..55b1b7a 100644 --- a/Glamourer/Gui/ToggleDrawData.cs +++ b/Glamourer/Gui/ToggleDrawData.cs @@ -1,121 +1,115 @@ using Glamourer.Designs; -using Glamourer.Events; using Glamourer.State; using Penumbra.GameData.Enums; namespace Glamourer.Gui; -public ref struct ToggleDrawData +public struct ToggleDrawData { + private IDesignEditor _editor = null!; + private object _data = null!; + private StateIndex _index; + public bool Locked; public bool DisplayApplication; public bool CurrentValue; public bool CurrentApply; - public Action SetValue = null!; - public Action SetApply = null!; - public string Label = string.Empty; public string Tooltip = string.Empty; + public ToggleDrawData() { } - public static ToggleDrawData FromDesign(ActorState.MetaIndex index, DesignManager manager, Design design) + public readonly void SetValue(bool value) { - var (label, value, apply, setValue, setApply) = index switch + switch (_index.GetFlag()) { - ActorState.MetaIndex.HatState => ("Hat Visible", design.DesignData.IsHatVisible(), design.DoApplyHatVisible(), - (Action)(b => manager.ChangeMeta(design, index, b)), (Action)(b => manager.ChangeApplyMeta(design, index, b))), - ActorState.MetaIndex.VisorState => ("Visor Toggled", design.DesignData.IsVisorToggled(), design.DoApplyVisorToggle(), - b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), - ActorState.MetaIndex.WeaponState => ("Weapon Visible", design.DesignData.IsWeaponVisible(), design.DoApplyWeaponVisible(), - b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), - ActorState.MetaIndex.Wetness => ("Force Wetness", design.DesignData.IsWet(), design.DoApplyWetness(), - b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), - _ => throw new Exception("Unsupported meta index."), - }; + case MetaIndex index: + _editor.ChangeMetaState(_data, index, value, ApplySettings.Manual); + break; + case CrestFlag flag: + _editor.ChangeCrest(_data, flag, value, ApplySettings.Manual); + break; + } + } - return new ToggleDrawData + public readonly void SetApply(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_data; + switch (_index.GetFlag()) { - Label = label, + case MetaIndex index: + manager.ChangeApplyMeta(design, index, value); + break; + case CrestFlag flag: + manager.ChangeApplyCrest(design, flag, value); + break; + } + } + + public static ToggleDrawData FromDesign(MetaIndex index, DesignManager manager, Design design) + => new() + { + _index = index, + _editor = manager, + _data = design, + Label = index.ToName(), Tooltip = string.Empty, Locked = design.WriteProtected(), DisplayApplication = true, - CurrentValue = value, - CurrentApply = apply, - SetValue = setValue, - SetApply = setApply, + CurrentValue = design.DesignData.GetMeta(index), + CurrentApply = design.DoApplyMeta(index), + }; + + public static ToggleDrawData FromState(MetaIndex index, StateManager manager, ActorState state) + => new() + { + _index = index, + _editor = manager, + _data = state, + Label = index.ToName(), + Tooltip = index.ToTooltip(), + Locked = state.IsLocked, + CurrentValue = state.ModelData.GetMeta(index), }; - } public static ToggleDrawData CrestFromDesign(CrestFlag slot, DesignManager manager, Design design) => new() { + _index = slot, + _editor = manager, + _data = design, Label = $"{slot.ToLabel()} Crest", Tooltip = string.Empty, Locked = design.WriteProtected(), DisplayApplication = true, CurrentValue = design.DesignData.Crest(slot), CurrentApply = design.DoApplyCrest(slot), - SetValue = v => manager.ChangeCrest(design, slot, v), - SetApply = v => manager.ChangeApplyCrest(design, slot, v), }; public static ToggleDrawData CrestFromState(CrestFlag slot, StateManager manager, ActorState state) => new() { + _index = slot, + _editor = manager, + _data = state, Label = $"{slot.ToLabel()} Crest", Tooltip = "Hide or show your free company crest on this piece of gear.", Locked = state.IsLocked, CurrentValue = state.ModelData.Crest(slot), - SetValue = v => manager.ChangeCrest(state, slot, v, StateChanged.Source.Manual), }; - public static ToggleDrawData FromState(ActorState.MetaIndex index, StateManager manager, ActorState state) - { - var (label, tooltip, value, setValue) = index switch + public static ToggleDrawData FromValue(MetaIndex index, bool value) + => new() { - ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear.", state.ModelData.IsHatVisible(), - (Action)(b => manager.ChangeHatState(state, b, StateChanged.Source.Manual))), - ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear.", - state.ModelData.IsVisorToggled(), - b => manager.ChangeVisorState(state, b, StateChanged.Source.Manual)), - ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn.", - state.ModelData.IsWeaponVisible(), - b => manager.ChangeWeaponState(state, b, StateChanged.Source.Manual)), - ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not.", state.ModelData.IsWet(), - b => manager.ChangeWetness(state, b, StateChanged.Source.Manual)), - _ => throw new Exception("Unsupported meta index."), - }; - - return new ToggleDrawData - { - Label = label, - Tooltip = tooltip, - Locked = state.IsLocked, - CurrentValue = value, - SetValue = setValue, - }; - } - - public static ToggleDrawData FromValue(ActorState.MetaIndex index, bool value) - { - var (label, tooltip) = index switch - { - ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear."), - ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear."), - ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn."), - ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not."), - _ => throw new Exception("Unsupported meta index."), - }; - return new ToggleDrawData - { - Label = label, - Tooltip = tooltip, + _index = index, + Label = index.ToName(), + Tooltip = index.ToTooltip(), Locked = true, CurrentValue = value, }; - } } diff --git a/Glamourer/Interop/CharaFile/CharaFile.cs b/Glamourer/Interop/CharaFile/CharaFile.cs index 8e25d8e..0613fb3 100644 --- a/Glamourer/Interop/CharaFile/CharaFile.cs +++ b/Glamourer/Interop/CharaFile/CharaFile.cs @@ -14,24 +14,17 @@ public sealed class CharaFile public CustomizeFlag ApplyCustomize; public EquipFlag ApplyEquip; - public static CharaFile? ParseData(ItemManager items, string data, string? name = null) + public static CharaFile ParseData(ItemManager items, string data, string? name = null) { - try - { - var jObj = JObject.Parse(data); - SanityCheck(jObj); - var ret = new CharaFile(); - ret.Data.SetDefaultEquipment(items); - ret.Data.ModelId = ParseModelId(jObj); - ret.Name = jObj["Nickname"]?.ToObject() ?? name ?? "New Design"; - ret.ApplyCustomize = ParseCustomize(jObj, ref ret.Data.Customize); - ret.ApplyEquip = ParseEquipment(items, jObj, ref ret.Data); - return ret; - } - catch - { - return null; - } + var jObj = JObject.Parse(data); + SanityCheck(jObj); + var ret = new CharaFile(); + ret.Data.SetDefaultEquipment(items); + ret.Data.ModelId = ParseModelId(jObj); + ret.Name = jObj["Nickname"]?.ToObject() ?? name ?? "New Design"; + ret.ApplyCustomize = ParseCustomize(jObj, ref ret.Data.Customize); + ret.ApplyEquip = ParseEquipment(items, jObj, ref ret.Data); + return ret; } private static EquipFlag ParseEquipment(ItemManager items, JObject jObj, ref DesignData data) @@ -282,9 +275,6 @@ public sealed class CharaFile private static void SanityCheck(JObject jObj) { - if (jObj["TypeName"]?.ToObject() is not "Anamnesis Character File") - throw new Exception("Wrong TypeName property set."); - var type = jObj["ObjectKind"]?.ToObject(); if (type is not "Player") throw new Exception($"ObjectKind {type} != Player is not supported."); diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs index 228eda7..3cfca50 100644 --- a/Glamourer/Interop/ContextMenuService.cs +++ b/Glamourer/Interop/ContextMenuService.cs @@ -3,7 +3,7 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using Glamourer.Events; +using Glamourer.Designs; using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; @@ -118,14 +118,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, ApplySettings.Manual); if (item.Type.ValidOffhand().IsOffhandType()) { if (item.PrimaryId.Id is > 1600 and < 1651 && _items.ItemData.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, ApplySettings.Manual); if (_items.ItemData.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, ApplySettings.Manual); } }; } @@ -142,14 +142,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, ApplySettings.Manual); if (item.Type.ValidOffhand().IsOffhandType()) { if (item.PrimaryId.Id is > 1600 and < 1651 && _items.ItemData.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, ApplySettings.Manual); if (_items.ItemData.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, ApplySettings.Manual); } }; } diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs index b31a1b0..9587feb 100644 --- a/Glamourer/Interop/ImportService.cs +++ b/Glamourer/Interop/ImportService.cs @@ -63,8 +63,6 @@ public class ImportService(CustomizeService _customizations, IDragDropManager _d { var text = File.ReadAllText(path); var file = CharaFile.CharaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); - if (file == null) - throw new Exception(); name = file.Name; design = new DesignBase(_customizations, file.Data, file.ApplyEquip, file.ApplyCustomize); diff --git a/Glamourer/Interop/PalettePlus/PaletteImport.cs b/Glamourer/Interop/PalettePlus/PaletteImport.cs index 474527b..8513036 100644 --- a/Glamourer/Interop/PalettePlus/PaletteImport.cs +++ b/Glamourer/Interop/PalettePlus/PaletteImport.cs @@ -1,7 +1,6 @@ using Dalamud.Plugin; using Glamourer.Designs; using Glamourer.GameData; -using Glamourer.State; using Newtonsoft.Json.Linq; using OtterGui.Services; @@ -41,9 +40,9 @@ public class PaletteImport(DalamudPluginInterface pluginInterface, DesignManager design.ApplyCustomize = 0; design.ApplyEquip = 0; design.ApplyCrest = 0; - designManager.ChangeApplyMeta(design, ActorState.MetaIndex.VisorState, false); - designManager.ChangeApplyMeta(design, ActorState.MetaIndex.HatState, false); - designManager.ChangeApplyMeta(design, ActorState.MetaIndex.WeaponState, false); + designManager.ChangeApplyMeta(design, MetaIndex.VisorState, false); + designManager.ChangeApplyMeta(design, MetaIndex.HatState, false); + designManager.ChangeApplyMeta(design, MetaIndex.WeaponState, false); foreach (var flag in flags.Iterate()) { designManager.ChangeApplyParameter(design, flag, true); diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index fa08425..eb0e792 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -255,26 +255,26 @@ public class CommandService : IDisposable } --designIdx; - AutoDesign.Type applicationFlags = 0; + ApplicationType applicationFlags = 0; if (split2.Length == 2) foreach (var character in split2[1]) { switch (char.ToLowerInvariant(character)) { case 'c': - applicationFlags |= AutoDesign.Type.Customizations; + applicationFlags |= ApplicationType.Customizations; break; case 'e': - applicationFlags |= AutoDesign.Type.Armor; + applicationFlags |= ApplicationType.Armor; break; case 'a': - applicationFlags |= AutoDesign.Type.Accessories; + applicationFlags |= ApplicationType.Accessories; break; case 'd': - applicationFlags |= AutoDesign.Type.GearCustomization; + applicationFlags |= ApplicationType.GearCustomization; break; case 'w': - applicationFlags |= AutoDesign.Type.Weapons; + applicationFlags |= ApplicationType.Weapons; break; default: _chat.Print(new SeStringBuilder().AddText("The value ").AddPurple(split2[1], true) @@ -333,7 +333,7 @@ public class CommandService : IDisposable foreach (var identifier in identifiers) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ResetState(state, StateChanged.Source.Manual); + _stateManager.ResetState(state, StateSource.Manual); } @@ -419,7 +419,7 @@ public class CommandService : IDisposable if (!_objects.TryGetValue(identifier, out var actors)) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, design, ApplySettings.Manual with { MergeLinks = true }); } else { @@ -428,7 +428,7 @@ public class CommandService : IDisposable if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) { ApplyModSettings(design, actor, applyMods); - _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, design, ApplySettings.Manual with { MergeLinks = true }); } } } @@ -587,7 +587,7 @@ public class CommandService : IDisposable if (Guid.TryParse(argument, out var guid)) { - design = _designManager.Designs.FirstOrDefault(d => d.Identifier == guid); + design = _designManager.Designs.ByIdentifier(guid); } else { diff --git a/Glamourer/Services/IGamePathParser.cs b/Glamourer/Services/IGamePathParser.cs deleted file mode 100644 index 28364d1..0000000 --- a/Glamourer/Services/IGamePathParser.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Glamourer.Services -{ - internal interface IGamePathParser - { - } -} \ No newline at end of file diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 2f64cf8..d9d76b4 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -121,7 +121,7 @@ public static class ServiceManagerA private static ServiceManager AddState(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index ef00f38..b5126da 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -1,5 +1,4 @@ using Glamourer.Designs; -using Glamourer.Events; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Dalamud.Game.ClientState.Conditions; @@ -11,15 +10,6 @@ namespace Glamourer.State; public class ActorState { - public enum MetaIndex - { - Wetness = EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices, - HatState, - VisorState, - WeaponState, - ModelId, - } - public readonly ActorIdentifier Identifier; public bool AllowsRedraw(ICondition condition) @@ -77,47 +67,14 @@ public class ActorState => Unlock(1337); /// This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. - private readonly StateChanged.Source[] _sources = Enumerable - .Repeat(StateChanged.Source.Game, - EquipFlagExtensions.NumEquipFlags - + CustomizationExtensions.NumIndices - + 5 - + CrestExtensions.AllRelevantSet.Count - + CustomizeParameterExtensions.AllFlags.Count).ToArray(); + public StateSources Sources = new(); 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[CrestFlag slot] - => ref _sources[EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5 + slot.ToInternalIndex()]; - - public ref StateChanged.Source this[CustomizeIndex type] - => ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type]; - - public ref StateChanged.Source this[MetaIndex index] - => ref _sources[(int)index]; - - public ref StateChanged.Source this[CustomizeParameterFlag flag] - => ref _sources[ - EquipFlagExtensions.NumEquipFlags - + CustomizationExtensions.NumIndices + 5 - + CrestExtensions.AllRelevantSet.Count - + flag.ToInternalIndex()]; - - public void RemoveFixedDesignSources() - { - for (var i = 0; i < _sources.Length; ++i) - { - if (_sources[i] is StateChanged.Source.Fixed) - _sources[i] = StateChanged.Source.Manual; - } - } - public CustomizeParameterFlag OnlyChangedParameters() - => CustomizeParameterExtensions.AllFlags.Where(f => this[f] is not StateChanged.Source.Game).Aggregate((CustomizeParameterFlag) 0, (a, b) => a | b); + => CustomizeParameterExtensions.AllFlags.Where(f => Sources[f] is not StateSource.Game) + .Aggregate((CustomizeParameterFlag)0, (a, b) => a | b); public bool UpdateTerritory(ushort territory) { @@ -127,4 +84,4 @@ public class ActorState LastTerritory = territory; return true; } -} +} \ No newline at end of file diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index f152528..14fc65a 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -116,8 +116,6 @@ public unsafe class FunModule : IDisposable SetRandomItem(slot, ref armor); break; case CodeService.CodeFlag.Elephants: - SetElephant(slot, ref armor); - break; case CodeService.CodeFlag.World when actor.Index != 0: KeepOldArmor(actor, slot, ref armor); break; @@ -166,8 +164,9 @@ public unsafe class FunModule : IDisposable SetRandomItem(slot, ref armor[idx]); break; case CodeService.CodeFlag.Elephants: - SetElephant(EquipSlot.Body, ref armor[1]); - SetElephant(EquipSlot.Head, ref armor[0]); + var stainId = ElephantStains[_rng.Next(0, ElephantStains.Length)]; + SetElephant(EquipSlot.Body, ref armor[1], stainId); + SetElephant(EquipSlot.Head, ref armor[0], stainId); break; case CodeService.CodeFlag.World when actor.Index != 0: _worldSets.Apply(actor, _rng, armor); @@ -216,12 +215,24 @@ public unsafe class FunModule : IDisposable armor.Variant = item.Variant; } - private static void SetElephant(EquipSlot slot, ref CharacterArmor armor) + private static ReadOnlySpan ElephantStains + => + [ + 87, 87, 87, 87, 87, // Cherry Pink + 83, 83, 83, // Colibri Pink + 80, // Iris Purple + 85, // Regal Purple + 103, // Pastel Pink + 82, 82, 82, // Lotus Pink + 7, // Rose Pink + ]; + + private void SetElephant(EquipSlot slot, ref CharacterArmor armor, StainId stainId) { armor = slot switch { - EquipSlot.Body => new CharacterArmor(6133, 1, 87), - EquipSlot.Head => new CharacterArmor(6133, 1, 87), + EquipSlot.Body => new CharacterArmor(6133, 1, stainId), + EquipSlot.Head => new CharacterArmor(6133, 1, stainId), _ => armor, }; } diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs new file mode 100644 index 0000000..2bc50c3 --- /dev/null +++ b/Glamourer/State/InternalStateEditor.cs @@ -0,0 +1,234 @@ +using Dalamud.Plugin.Services; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Services; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public class InternalStateEditor( + CustomizeService customizations, + HumanModelList humans, + ItemManager items, + GPoseService gPose, + ICondition condition) +{ + /// Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. + /// We currently only allow changing things to humans, not humans to monsters. + public bool ChangeModelId(ActorState state, uint modelId, in CustomizeArray customize, nint equipData, StateSource source, + out uint oldModelId, uint key = 0) + { + oldModelId = state.ModelData.ModelId; + + // TODO think about this. + if (modelId != 0) + return false; + + if (!state.CanUnlock(key)) + return false; + + var oldIsHuman = state.ModelData.IsHuman; + state.ModelData.IsHuman = humans.IsHuman(modelId); + if (state.ModelData.IsHuman) + { + if (oldModelId == modelId) + return true; + + state.ModelData.ModelId = modelId; + if (oldIsHuman) + return true; + + if (!state.AllowsRedraw(condition)) + return false; + + // Fix up everything else to make sure the result is a valid human. + state.ModelData.Customize = CustomizeArray.Default; + state.ModelData.SetDefaultEquipment(items); + state.ModelData.SetHatVisible(true); + state.ModelData.SetWeaponVisible(true); + state.ModelData.SetVisor(false); + state.Sources[MetaIndex.ModelId] = source; + state.Sources[MetaIndex.HatState] = source; + state.Sources[MetaIndex.WeaponState] = source; + state.Sources[MetaIndex.VisorState] = source; + foreach (var slot in EquipSlotExtensions.FullSlots) + { + state.Sources[slot, true] = source; + state.Sources[slot, false] = source; + } + + state.Sources[CustomizeIndex.Clan] = source; + state.Sources[CustomizeIndex.Gender] = source; + var set = customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); + foreach (var index in Enum.GetValues().Where(set.IsAvailable)) + state.Sources[index] = source; + } + else + { + if (!state.AllowsRedraw(condition)) + return false; + + state.ModelData.LoadNonHuman(modelId, customize, equipData); + state.Sources[MetaIndex.ModelId] = source; + } + + return true; + } + + /// Change a customization value. + public bool ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateSource source, + out CustomizeValue old, uint key = 0) + { + old = state.ModelData.Customize[idx]; + if (!state.CanUnlock(key)) + return false; + + state.ModelData.Customize[idx] = value; + state.Sources[idx] = source; + return true; + } + + /// Change an entire customization array according to functions. + public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag applyWhich, + Func source, out CustomizeArray old, out CustomizeFlag changed, uint key = 0) + { + old = state.ModelData.Customize; + changed = 0; + if (!state.CanUnlock(key)) + return false; + + (var customize, var applied, changed) = customizations.Combine(state.ModelData.Customize, customizeInput, applyWhich, true); + if (changed == 0) + return false; + + state.ModelData.Customize = customize; + applied |= changed; + foreach (var type in Enum.GetValues()) + { + if (applied.HasFlag(type.ToFlag())) + state.Sources[type] = source(type); + } + + return true; + } + + /// Change an entire customization array according to functions. + public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, Func applyWhich, + Func source, out CustomizeArray old, out CustomizeFlag changed, uint key = 0) + { + var apply = Enum.GetValues().Where(applyWhich).Aggregate((CustomizeFlag)0, (current, type) => current | type.ToFlag()); + return ChangeHumanCustomize(state, customizeInput, apply, source, out old, out changed, key); + } + + /// Change a single piece of equipment without stain. + public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateSource source, out EquipItem oldItem, uint key = 0) + { + oldItem = state.ModelData.Item(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 (!gPose.InGPose) + return false; + + var old = oldItem; + gPose.AddActionOnLeave(() => + { + if (old.Type == state.BaseData.Item(slot).Type) + ChangeItem(state, slot, old, state.Sources[slot, false], out _, key); + }); + } + + state.ModelData.SetItem(slot, item); + state.Sources[slot, false] = source; + return true; + } + + /// Change a single piece of equipment including stain. + public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateSource source, out EquipItem oldItem, + out StainId oldStain, uint key = 0) + { + oldItem = state.ModelData.Item(slot); + oldStain = state.ModelData.Stain(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 (!gPose.InGPose) + return false; + + var old = oldItem; + var oldS = oldStain; + gPose.AddActionOnLeave(() => + { + if (old.Type == state.BaseData.Item(slot).Type) + ChangeEquip(state, slot, old, oldS, state.Sources[slot, false], out _, out _, key); + }); + } + + state.ModelData.SetItem(slot, item); + state.ModelData.SetStain(slot, stain); + state.Sources[slot, false] = source; + state.Sources[slot, true] = source; + return true; + } + + /// Change only the stain of an equipment piece. + public bool ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateSource source, out StainId oldStain, uint key = 0) + { + oldStain = state.ModelData.Stain(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetStain(slot, stain); + state.Sources[slot, true] = source; + return true; + } + + /// Change the crest of an equipment piece. + public bool ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateSource source, out bool oldCrest, uint key = 0) + { + oldCrest = state.ModelData.Crest(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetCrest(slot, crest); + state.Sources[slot] = source; + return true; + } + + /// Change the customize flags of a character. + public bool ChangeParameter(ActorState state, CustomizeParameterFlag flag, CustomizeParameterValue value, StateSource source, + out CustomizeParameterValue oldValue, uint key = 0) + { + oldValue = state.ModelData.Parameters[flag]; + if (!state.CanUnlock(key)) + return false; + + state.ModelData.Parameters.Set(flag, value); + state.Sources[flag] = source; + + return true; + } + + public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, + uint key = 0) + { + oldValue = state.ModelData.GetMeta(index); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetMeta(index, value); + state.Sources[index] = source; + return true; + } +} diff --git a/Glamourer/State/JobChangeState.cs b/Glamourer/State/JobChangeState.cs new file mode 100644 index 0000000..84aa3cc --- /dev/null +++ b/Glamourer/State/JobChangeState.cs @@ -0,0 +1,30 @@ +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public sealed class JobChangeState : Dictionary, IService +{ + public ActorState? State { get; private set; } + + public void Reset() + { + State = null; + Clear(); + } + + public bool HasState + => State != null; + + public ActorIdentifier Identifier + => State?.Identifier ?? ActorIdentifier.Invalid; + + public void Set(ActorState state, IEnumerable<(EquipItem, StateSource)> items) + { + foreach (var (item, source) in items.Where(p => p.Item1.Valid)) + TryAdd(item.Type, (item, source)); + State = state; + } +} diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 8b69e0a..7222a4b 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -1,4 +1,4 @@ -using Glamourer.Events; +using Glamourer.Designs; using Glamourer.GameData; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -118,7 +118,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.Sources[slot, false] is not StateSource.Ipc, state.ModelData.IsHatVisible()); return data; @@ -200,67 +200,44 @@ public class StateApplier( _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); } - /// Change the visor state of actors only on the draw object. - public void ChangeVisor(ActorData data, bool value) + /// Change a meta state. + public unsafe void ChangeMetaState(ActorData data, MetaIndex index, bool value) { - foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _visor.SetVisorState(actor.Model, value); + switch (index) + { + case MetaIndex.Wetness: + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + actor.AsCharacter->IsGPoseWet = value; + return; + } + case MetaIndex.HatState: + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _metaService.SetHatState(actor, value); + return; + } + case MetaIndex.WeaponState: + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _metaService.SetWeaponState(actor, value); + return; + } + case MetaIndex.VisorState: + { + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + _visor.SetVisorState(actor.Model, value); + return; + } + } } - /// - public ActorData ChangeVisor(ActorState state, bool apply) + /// + public ActorData ChangeMetaState(ActorState state, MetaIndex index, bool apply) { var data = GetData(state); if (apply) - ChangeVisor(data, state.ModelData.IsVisorToggled()); - return data; - } - - /// Change the forced wetness state on actors. - public unsafe void ChangeWetness(ActorData data, bool value) - { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - actor.AsCharacter->IsGPoseWet = value; - } - - /// - public ActorData ChangeWetness(ActorState state, bool apply) - { - var data = GetData(state); - if (apply) - ChangeWetness(data, state.ModelData.IsWet()); - return data; - } - - /// Change the hat-visibility state on actors. - public void ChangeHatState(ActorData data, bool value) - { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _metaService.SetHatState(actor, value); - } - - /// - public ActorData ChangeHatState(ActorState state, bool apply) - { - var data = GetData(state); - if (apply) - ChangeHatState(data, state.ModelData.IsHatVisible()); - return data; - } - - /// Change the weapon-visibility state on actors. - public void ChangeWeaponState(ActorData data, bool value) - { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _metaService.SetWeaponState(actor, value); - } - - /// - public ActorData ChangeWeaponState(ActorState state, bool apply) - { - var data = GetData(state); - if (apply) - ChangeWeaponState(data, state.ModelData.IsWeaponVisible()); + ChangeMetaState(data, index, state.ModelData.GetMeta(index)); return data; } @@ -299,6 +276,47 @@ public class StateApplier( return data; } + /// Apply the entire state of an actor to all relevant actors, either via immediate redraw or piecewise. + /// The state to apply. + /// Whether a redraw should be forced. + /// Whether a temporary lock should be applied for the redraw. + /// The actor data for the actors who got changed. + public ActorData ApplyAll(ActorState state, bool redraw, bool withLock) + { + var actors = ChangeMetaState(state, MetaIndex.Wetness, true); + if (redraw) + { + if (withLock) + state.TempLock(); + ForceRedraw(actors); + } + else + { + ChangeCustomize(actors, state.ModelData.Customize); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.Sources[slot, false] is not StateSource.Ipc, + state.ModelData.IsHatVisible()); + } + + var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; + ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); + var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; + ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); + } + + if (state.ModelData.IsHuman) + { + ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); + ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); + ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); + ChangeCrests(actors, state.ModelData.CrestVisibility); + ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); + } + + return actors; + } + private ActorData GetData(ActorState state) { _objects.Update(); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 9dc8418..2b1337e 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,246 +1,326 @@ -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Common.Math; +using Glamourer.Designs; +using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Structs; using Glamourer.Services; -using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Vector3 = FFXIVClientStructs.FFXIV.Common.Math.Vector3; namespace Glamourer.State; -public class StateEditor +public class StateEditor( + InternalStateEditor editor, + StateApplier applier, + StateChanged stateChanged, + JobChangeState jobChange, + Configuration config, + ItemManager items, + DesignMerger merger) : IDesignEditor { - private readonly ItemManager _items; - private readonly CustomizeService _customizations; - private readonly HumanModelList _humans; - private readonly GPoseService _gPose; - private readonly ICondition _condition; + protected readonly InternalStateEditor Editor = editor; + protected readonly StateApplier Applier = applier; + protected readonly StateChanged StateChanged = stateChanged; + protected readonly Configuration Config = config; + protected readonly ItemManager Items = items; - public StateEditor(CustomizeService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition) - { - _customizations = customizations; - _humans = humans; - _items = items; - _gPose = gPose; - _condition = condition; - } - - /// Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. - /// We currently only allow changing things to humans, not humans to monsters. - public bool ChangeModelId(ActorState state, uint modelId, in CustomizeArray customize, nint equipData, StateChanged.Source source, - out uint oldModelId, uint key = 0) - { - oldModelId = state.ModelData.ModelId; - - // TODO think about this. - if (modelId != 0) - return false; - - if (!state.CanUnlock(key)) - return false; - - var oldIsHuman = state.ModelData.IsHuman; - state.ModelData.IsHuman = _humans.IsHuman(modelId); - if (state.ModelData.IsHuman) - { - if (oldModelId == modelId) - return true; - - state.ModelData.ModelId = modelId; - if (oldIsHuman) - return true; - - if (!state.AllowsRedraw(_condition)) - return false; - - // Fix up everything else to make sure the result is a valid human. - state.ModelData.Customize = CustomizeArray.Default; - state.ModelData.SetDefaultEquipment(_items); - state.ModelData.SetHatVisible(true); - state.ModelData.SetWeaponVisible(true); - state.ModelData.SetVisor(false); - state[ActorState.MetaIndex.ModelId] = source; - state[ActorState.MetaIndex.HatState] = source; - state[ActorState.MetaIndex.WeaponState] = source; - state[ActorState.MetaIndex.VisorState] = source; - foreach (var slot in EquipSlotExtensions.FullSlots) - { - state[slot, true] = source; - state[slot, false] = source; - } - - state[CustomizeIndex.Clan] = source; - state[CustomizeIndex.Gender] = source; - var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); - foreach (var index in Enum.GetValues().Where(set.IsAvailable)) - state[index] = source; - } - else - { - if (!state.AllowsRedraw(_condition)) - return false; - - state.ModelData.LoadNonHuman(modelId, customize, equipData); - state[ActorState.MetaIndex.ModelId] = source; - } - - return true; - } - - /// Change a customization value. - public bool ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, - out CustomizeValue old, uint key = 0) - { - old = state.ModelData.Customize[idx]; - if (!state.CanUnlock(key)) - return false; - - state.ModelData.Customize[idx] = value; - state[idx] = source; - return true; - } - - /// Change an entire customization array according to flags. - public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag applyWhich, StateChanged.Source source, - out CustomizeArray old, out CustomizeFlag changed, uint key = 0) - { - old = state.ModelData.Customize; - changed = 0; - if (!state.CanUnlock(key)) - return false; - - (var customize, var applied, changed) = _customizations.Combine(state.ModelData.Customize, customizeInput, applyWhich, true); - if (changed == 0) - return false; - - state.ModelData.Customize = customize; - applied |= changed; - foreach (var type in Enum.GetValues()) - { - if (applied.HasFlag(type.ToFlag())) - state[type] = source; - } - - return true; - } - - /// Change a single piece of equipment without stain. - public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, out EquipItem oldItem, uint key = 0) - { - oldItem = state.ModelData.Item(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 (!_gPose.InGPose) - return false; - - var old = oldItem; - _gPose.AddActionOnLeave(() => - { - if (old.Type == state.BaseData.Item(slot).Type) - ChangeItem(state, slot, old, state[slot, false], out _, key); - }); - } - - state.ModelData.SetItem(slot, item); - state[slot, false] = 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) - { - oldItem = state.ModelData.Item(slot); - oldStain = state.ModelData.Stain(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 (!_gPose.InGPose) - return false; - - var old = oldItem; - var oldS = oldStain; - _gPose.AddActionOnLeave(() => - { - if (old.Type == state.BaseData.Item(slot).Type) - ChangeEquip(state, slot, old, oldS, state[slot, false], out _, out _, key); - }); - } - - state.ModelData.SetItem(slot, item); - state.ModelData.SetStain(slot, stain); - state[slot, false] = source; - state[slot, true] = source; - return true; - } - - /// Change only the stain of an equipment piece. - public bool ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source, out StainId oldStain, uint key = 0) - { - oldStain = state.ModelData.Stain(slot); - if (!state.CanUnlock(key)) - return false; - - state.ModelData.SetStain(slot, stain); - state[slot, true] = source; - return true; - } - - /// Change the crest of an equipment piece. - public bool ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, out bool oldCrest, uint key = 0) - { - oldCrest = state.ModelData.Crest(slot); - if (!state.CanUnlock(key)) - return false; - - state.ModelData.SetCrest(slot, crest); - state[slot] = source; - return true; - } - - /// Change the customize flags of a character. - public bool ChangeParameter(ActorState state, CustomizeParameterFlag flag, CustomizeParameterValue value, StateChanged.Source source, - out CustomizeParameterValue oldValue, uint key = 0) - { - oldValue = state.ModelData.Parameters[flag]; - if (!state.CanUnlock(key)) - return false; - - state.ModelData.Parameters.Set(flag, value); - state[flag] = source; - - return true; - } - - public bool ChangeMetaState(ActorState state, ActorState.MetaIndex index, bool value, StateChanged.Source source, out bool oldValue, + /// Turn an actor to. + public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source, uint key = 0) { - (var setter, oldValue) = index switch + if (!Editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) + return; + + var actors = Applier.ForceRedraw(state, source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId)); + } + + /// + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeCustomize(state, idx, value, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Customize, settings.Source, state, actors, (old, value, idx)); + } + + /// + public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeHumanCustomize(state, customizeInput, apply, _ => settings.Source, out var old, out var applied, settings.Key)) + return; + + var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.EntireCustomize, settings.Source, state, actors, (old, applied)); + } + + /// + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) + { + var state = (ActorState)data; + if (!Editor.ChangeItem(state, slot, item, settings.Source, out var old, settings.Key)) + return; + + var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; + var actors = type is StateChanged.Type.Equip + ? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc) + : Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc, + item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); + + if (slot is EquipSlot.MainHand) + ApplyMainhandPeriphery(state, item, settings); + + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(type, settings.Source, state, actors, (old, item, slot)); + } + + /// + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings settings) + { + switch (item.HasValue, stain.HasValue) { - ActorState.MetaIndex.Wetness => ((Func)(v => state.ModelData.SetIsWet(v)), state.ModelData.IsWet()), - ActorState.MetaIndex.HatState => ((Func)(v => state.ModelData.SetHatVisible(v)), state.ModelData.IsHatVisible()), - ActorState.MetaIndex.VisorState => ((Func)(v => state.ModelData.SetVisor(v)), state.ModelData.IsVisorToggled()), - ActorState.MetaIndex.WeaponState => ((Func)(v => state.ModelData.SetWeaponVisible(v)), - state.ModelData.IsWeaponVisible()), - _ => throw new Exception("Invalid MetaIndex."), - }; + case (false, false): return; + case (true, false): + ChangeItem(data, slot, item!.Value, settings); + return; + case (false, true): + ChangeStain(data, slot, stain!.Value, settings); + return; + } - if (!state.CanUnlock(key)) - return false; + var state = (ActorState)data; + if (!Editor.ChangeEquip(state, slot, item ?? state.ModelData.Item(slot), stain ?? state.ModelData.Stain(slot), settings.Source, + out var old, out var oldStain, settings.Key)) + return; - setter(value); - state[index] = source; - return true; + var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; + var actors = type is StateChanged.Type.Equip + ? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc) + : Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc, + item!.Value.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); + + if (slot is EquipSlot.MainHand) + ApplyMainhandPeriphery(state, item, settings); + + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStain.Id} to {stain!.Value.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(type, settings.Source, state, actors, (old, item!.Value, slot)); + StateChanged.Invoke(StateChanged.Type.Stain, settings.Source, state, actors, (oldStain, stain!.Value, slot)); + } + + /// + public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeStain(state, slot, stain, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeStain(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Stain, settings.Source, state, actors, (old, stain, slot)); + } + + /// + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeCrest(state, slot, crest, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeCrests(state, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Crest, settings.Source, state, actors, (old, crest, slot)); + } + + /// + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings settings) + { + if (data is not ActorState state) + return; + + // Also apply main color to highlights when highlights is off. + if (!state.ModelData.Customize.Highlights && flag is CustomizeParameterFlag.HairDiffuse) + ChangeCustomizeParameter(state, CustomizeParameterFlag.HairHighlight, value, settings); + + if (!Editor.ChangeParameter(state, flag, value, settings.Source, out var old, settings.Key)) + return; + + var @new = state.ModelData.Parameters[flag]; + var actors = Applier.ChangeParameters(state, flag, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set {flag} crest in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); + } + + /// + public void ChangeMetaState(object data, MetaIndex index, bool value, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeMetaState(state, index, value, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeMetaState(state, index, settings.Source is StateSource.Manual or StateSource.Ipc); + Glamourer.Log.Verbose( + $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Other, settings.Source, state, actors, (old, value, MetaIndex.HatState)); + } + + /// + public void ApplyDesign(object data, MergedDesign mergedDesign, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeModelId(state, mergedDesign.Design.DesignData.ModelId, mergedDesign.Design.DesignData.Customize, + mergedDesign.Design.GetDesignDataRef().GetEquipmentPtr(), settings.Source, out var oldModelId, settings.Key)) + return; + + var requiresRedraw = oldModelId != mergedDesign.Design.DesignData.ModelId || !mergedDesign.Design.DesignData.IsHuman; + + if (state.ModelData.IsHuman) + { + foreach (var slot in CrestExtensions.AllRelevantSet.Where(mergedDesign.Design.DoApplyCrest)) + { + if (!settings.RespectManual || state.Sources[slot] is not StateSource.Manual) + Editor.ChangeCrest(state, slot, mergedDesign.Design.DesignData.Crest(slot), Source(slot), + out _, settings.Key); + } + + var customizeFlags = mergedDesign.Design.ApplyCustomizeRaw; + if (mergedDesign.Design.DoApplyCustomize(CustomizeIndex.Clan)) + customizeFlags |= CustomizeFlag.Race; + + Func applyWhich = settings.RespectManual + ? i => customizeFlags.HasFlag(i.ToFlag()) && state.Sources[i] is not StateSource.Manual + : i => customizeFlags.HasFlag(i.ToFlag()); + + if (Editor.ChangeHumanCustomize(state, mergedDesign.Design.DesignData.Customize, applyWhich, i => Source(i), out _, out var changed, + settings.Key)) + requiresRedraw |= changed.RequiresRedraw(); + + foreach (var parameter in mergedDesign.Design.ApplyParameters.Iterate()) + { + if (settings.RespectManual && state.Sources[parameter] is StateSource.Manual or StateSource.Pending) + continue; + + var source = Source(parameter); + if (source is StateSource.Manual) + source = StateSource.Pending; + Editor.ChangeParameter(state, parameter, mergedDesign.Design.DesignData.Parameters[parameter], source, out _, settings.Key); + } + + // Do not apply highlights from a design if highlights is unchecked. + if (!state.ModelData.Customize.Highlights) + Editor.ChangeParameter(state, CustomizeParameterFlag.HairHighlight, + state.ModelData.Parameters[CustomizeParameterFlag.HairDiffuse], + state.Sources[CustomizeParameterFlag.HairDiffuse], out _, settings.Key); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + if (mergedDesign.Design.DoApplyEquip(slot)) + if (!settings.RespectManual || state.Sources[slot, false] is not StateSource.Manual) + Editor.ChangeItem(state, slot, mergedDesign.Design.DesignData.Item(slot), + Source(slot.ToState()), out _, settings.Key); + + if (mergedDesign.Design.DoApplyStain(slot)) + if (!settings.RespectManual || state.Sources[slot, true] is not StateSource.Manual) + Editor.ChangeStain(state, slot, mergedDesign.Design.DesignData.Stain(slot), + Source(slot.ToState(true)), out _, settings.Key); + } + + foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots) + { + if (mergedDesign.Design.DoApplyStain(weaponSlot)) + if (!settings.RespectManual || state.Sources[weaponSlot, true] is not StateSource.Manual) + Editor.ChangeStain(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot), + Source(weaponSlot.ToState(true)), out _, settings.Key); + + if (!mergedDesign.Design.DoApplyEquip(weaponSlot)) + continue; + + if (settings.RespectManual && state.Sources[weaponSlot, false] is StateSource.Manual) + continue; + + var currentType = state.ModelData.Item(weaponSlot).Type; + if (!settings.FromJobChange && mergedDesign.Weapons.TryGetValue(currentType, out var weapon)) + { + var source = settings.UseSingleSource ? settings.Source : + weapon.Item2 is StateSource.Game ? StateSource.Game : weapon.Item2; + Editor.ChangeItem(state, weaponSlot, weapon.Item1, source, out _, + settings.Key); + } + } + + if (settings.FromJobChange) + jobChange.Set(state, mergedDesign.Weapons.Values.Select(m => + (m.Item1, settings.UseSingleSource ? settings.Source : + m.Item2 is StateSource.Game ? StateSource.Game : m.Item2))); + + foreach (var meta in MetaExtensions.AllRelevant) + { + if (!settings.RespectManual || state.Sources[meta] is not StateSource.Manual) + Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); + } + } + + var actors = settings.Source is StateSource.Manual or StateSource.Ipc + ? Applier.ApplyAll(state, requiresRedraw, false) + : ActorData.Invalid; + + Glamourer.Log.Verbose( + $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.Design, state.Sources[MetaIndex.Wetness], state, actors, mergedDesign.Design); + + return; + + StateSource Source(StateIndex index) + { + if (settings.UseSingleSource) + return settings.Source; + + var source = mergedDesign.Sources[index]; + return source is StateSource.Game ? StateSource.Game : settings.Source; + } + } + + public void ApplyDesign(object data, DesignBase design, ApplySettings settings) + { + var merged = settings.MergeLinks && design is Design d + ? merger.Merge(d.AllLinks, ((ActorState)data).ModelData, false, false) + : new MergedDesign(design); + + ApplyDesign(data, merged, settings with + { + FromJobChange = false, + RespectManual = false, + UseSingleSource = true, + }); + } + + + /// Apply offhand item and potentially gauntlets if configured. + private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, ApplySettings settings) + { + if (!Config.ChangeEntireItem || settings.Source is not StateSource.Manual) + return; + + var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); + var offhand = newMainhand != null ? Items.GetDefaultOffhand(mh) : state.ModelData.Item(EquipSlot.OffHand); + if (offhand.Valid) + ChangeEquip(state, EquipSlot.OffHand, offhand, state.ModelData.Stain(EquipSlot.OffHand), settings); + + if (mh is { Type: FullEquipType.Fists } && Items.ItemData.Tertiary.TryGetValue(mh.ItemId, out var gauntlets)) + ChangeEquip(state, EquipSlot.Hands, newMainhand != null ? gauntlets : state.ModelData.Item(EquipSlot.Hands), + state.ModelData.Stain(EquipSlot.Hands), settings); } } diff --git a/Glamourer/State/StateIndex.cs b/Glamourer/State/StateIndex.cs new file mode 100644 index 0000000..a55d6b1 --- /dev/null +++ b/Glamourer/State/StateIndex.cs @@ -0,0 +1,456 @@ +using Glamourer.Designs; +using Glamourer.GameData; +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public readonly record struct StateIndex(int Value) : IEqualityOperators +{ + public static readonly StateIndex Invalid = new(-1); + + public static implicit operator StateIndex(EquipFlag flag) + => flag switch + { + EquipFlag.Head => new StateIndex(EquipHead), + EquipFlag.Body => new StateIndex(EquipBody), + EquipFlag.Hands => new StateIndex(EquipHands), + EquipFlag.Legs => new StateIndex(EquipLegs), + EquipFlag.Feet => new StateIndex(EquipFeet), + EquipFlag.Ears => new StateIndex(EquipEars), + EquipFlag.Neck => new StateIndex(EquipNeck), + EquipFlag.Wrist => new StateIndex(EquipWrist), + EquipFlag.RFinger => new StateIndex(EquipRFinger), + EquipFlag.LFinger => new StateIndex(EquipLFinger), + EquipFlag.Mainhand => new StateIndex(EquipMainhand), + EquipFlag.Offhand => new StateIndex(EquipOffhand), + EquipFlag.HeadStain => new StateIndex(StainHead), + EquipFlag.BodyStain => new StateIndex(StainBody), + EquipFlag.HandsStain => new StateIndex(StainHands), + EquipFlag.LegsStain => new StateIndex(StainLegs), + EquipFlag.FeetStain => new StateIndex(StainFeet), + EquipFlag.EarsStain => new StateIndex(StainEars), + EquipFlag.NeckStain => new StateIndex(StainNeck), + EquipFlag.WristStain => new StateIndex(StainWrist), + EquipFlag.RFingerStain => new StateIndex(StainRFinger), + EquipFlag.LFingerStain => new StateIndex(StainLFinger), + EquipFlag.MainhandStain => new StateIndex(StainMainhand), + EquipFlag.OffhandStain => new StateIndex(StainOffhand), + _ => Invalid, + }; + + public static implicit operator StateIndex(CustomizeIndex index) + => index switch + { + CustomizeIndex.Race => new StateIndex(CustomizeRace), + CustomizeIndex.Gender => new StateIndex(CustomizeGender), + CustomizeIndex.BodyType => new StateIndex(CustomizeBodyType), + CustomizeIndex.Height => new StateIndex(CustomizeHeight), + CustomizeIndex.Clan => new StateIndex(CustomizeClan), + CustomizeIndex.Face => new StateIndex(CustomizeFace), + CustomizeIndex.Hairstyle => new StateIndex(CustomizeHairstyle), + CustomizeIndex.Highlights => new StateIndex(CustomizeHighlights), + CustomizeIndex.SkinColor => new StateIndex(CustomizeSkinColor), + CustomizeIndex.EyeColorRight => new StateIndex(CustomizeEyeColorRight), + CustomizeIndex.HairColor => new StateIndex(CustomizeHairColor), + CustomizeIndex.HighlightsColor => new StateIndex(CustomizeHighlightsColor), + CustomizeIndex.FacialFeature1 => new StateIndex(CustomizeFacialFeature1), + CustomizeIndex.FacialFeature2 => new StateIndex(CustomizeFacialFeature2), + CustomizeIndex.FacialFeature3 => new StateIndex(CustomizeFacialFeature3), + CustomizeIndex.FacialFeature4 => new StateIndex(CustomizeFacialFeature4), + CustomizeIndex.FacialFeature5 => new StateIndex(CustomizeFacialFeature5), + CustomizeIndex.FacialFeature6 => new StateIndex(CustomizeFacialFeature6), + CustomizeIndex.FacialFeature7 => new StateIndex(CustomizeFacialFeature7), + CustomizeIndex.LegacyTattoo => new StateIndex(CustomizeLegacyTattoo), + CustomizeIndex.TattooColor => new StateIndex(CustomizeTattooColor), + CustomizeIndex.Eyebrows => new StateIndex(CustomizeEyebrows), + CustomizeIndex.EyeColorLeft => new StateIndex(CustomizeEyeColorLeft), + CustomizeIndex.EyeShape => new StateIndex(CustomizeEyeShape), + CustomizeIndex.SmallIris => new StateIndex(CustomizeSmallIris), + CustomizeIndex.Nose => new StateIndex(CustomizeNose), + CustomizeIndex.Jaw => new StateIndex(CustomizeJaw), + CustomizeIndex.Mouth => new StateIndex(CustomizeMouth), + CustomizeIndex.Lipstick => new StateIndex(CustomizeLipstick), + CustomizeIndex.LipColor => new StateIndex(CustomizeLipColor), + CustomizeIndex.MuscleMass => new StateIndex(CustomizeMuscleMass), + CustomizeIndex.TailShape => new StateIndex(CustomizeTailShape), + CustomizeIndex.BustSize => new StateIndex(CustomizeBustSize), + CustomizeIndex.FacePaint => new StateIndex(CustomizeFacePaint), + CustomizeIndex.FacePaintReversed => new StateIndex(CustomizeFacePaintReversed), + CustomizeIndex.FacePaintColor => new StateIndex(CustomizeFacePaintColor), + _ => Invalid, + }; + + public static implicit operator StateIndex(MetaIndex meta) + => new((int)meta); + + public static implicit operator StateIndex(CrestFlag crest) + => crest switch + { + CrestFlag.OffHand => new StateIndex(CrestOffhand), + CrestFlag.Head => new StateIndex(CrestHead), + CrestFlag.Body => new StateIndex(CrestBody), + _ => Invalid, + }; + + public static implicit operator StateIndex(CustomizeParameterFlag param) + => param switch + { + CustomizeParameterFlag.SkinDiffuse => new StateIndex(ParamSkinDiffuse), + CustomizeParameterFlag.MuscleTone => new StateIndex(ParamMuscleTone), + CustomizeParameterFlag.SkinSpecular => new StateIndex(ParamSkinSpecular), + CustomizeParameterFlag.LipDiffuse => new StateIndex(ParamLipDiffuse), + CustomizeParameterFlag.HairDiffuse => new StateIndex(ParamHairDiffuse), + CustomizeParameterFlag.HairSpecular => new StateIndex(ParamHairSpecular), + CustomizeParameterFlag.HairHighlight => new StateIndex(ParamHairHighlight), + CustomizeParameterFlag.LeftEye => new StateIndex(ParamLeftEye), + CustomizeParameterFlag.RightEye => new StateIndex(ParamRightEye), + CustomizeParameterFlag.FeatureColor => new StateIndex(ParamFeatureColor), + CustomizeParameterFlag.FacePaintUvMultiplier => new StateIndex(ParamFacePaintUvMultiplier), + CustomizeParameterFlag.FacePaintUvOffset => new StateIndex(ParamFacePaintUvOffset), + CustomizeParameterFlag.DecalColor => new StateIndex(ParamDecalColor), + _ => Invalid, + }; + + public const int EquipHead = 0; + public const int EquipBody = EquipHead + 1; + public const int EquipHands = EquipBody + 1; + public const int EquipLegs = EquipHands + 1; + public const int EquipFeet = EquipLegs + 1; + public const int EquipEars = EquipFeet + 1; + public const int EquipNeck = EquipEars + 1; + public const int EquipWrist = EquipNeck + 1; + public const int EquipRFinger = EquipWrist + 1; + public const int EquipLFinger = EquipRFinger + 1; + public const int EquipMainhand = EquipLFinger + 1; + public const int EquipOffhand = EquipMainhand + 1; + + public const int StainHead = EquipOffhand + 1; + public const int StainBody = StainHead + 1; + public const int StainHands = StainBody + 1; + public const int StainLegs = StainHands + 1; + public const int StainFeet = StainLegs + 1; + public const int StainEars = StainFeet + 1; + public const int StainNeck = StainEars + 1; + public const int StainWrist = StainNeck + 1; + public const int StainRFinger = StainWrist + 1; + public const int StainLFinger = StainRFinger + 1; + public const int StainMainhand = StainLFinger + 1; + public const int StainOffhand = StainMainhand + 1; + + public const int CustomizeRace = StainOffhand + 1; + public const int CustomizeGender = CustomizeRace + 1; + public const int CustomizeBodyType = CustomizeGender + 1; + public const int CustomizeHeight = CustomizeBodyType + 1; + public const int CustomizeClan = CustomizeHeight + 1; + public const int CustomizeFace = CustomizeClan + 1; + public const int CustomizeHairstyle = CustomizeFace + 1; + public const int CustomizeHighlights = CustomizeHairstyle + 1; + public const int CustomizeSkinColor = CustomizeHighlights + 1; + public const int CustomizeEyeColorRight = CustomizeSkinColor + 1; + public const int CustomizeHairColor = CustomizeEyeColorRight + 1; + public const int CustomizeHighlightsColor = CustomizeHairColor + 1; + public const int CustomizeFacialFeature1 = CustomizeHighlightsColor + 1; + public const int CustomizeFacialFeature2 = CustomizeFacialFeature1 + 1; + public const int CustomizeFacialFeature3 = CustomizeFacialFeature2 + 1; + public const int CustomizeFacialFeature4 = CustomizeFacialFeature3 + 1; + public const int CustomizeFacialFeature5 = CustomizeFacialFeature4 + 1; + public const int CustomizeFacialFeature6 = CustomizeFacialFeature5 + 1; + public const int CustomizeFacialFeature7 = CustomizeFacialFeature6 + 1; + public const int CustomizeLegacyTattoo = CustomizeFacialFeature7 + 1; + public const int CustomizeTattooColor = CustomizeLegacyTattoo + 1; + public const int CustomizeEyebrows = CustomizeTattooColor + 1; + public const int CustomizeEyeColorLeft = CustomizeEyebrows + 1; + public const int CustomizeEyeShape = CustomizeEyeColorLeft + 1; + public const int CustomizeSmallIris = CustomizeEyeShape + 1; + public const int CustomizeNose = CustomizeSmallIris + 1; + public const int CustomizeJaw = CustomizeNose + 1; + public const int CustomizeMouth = CustomizeJaw + 1; + public const int CustomizeLipstick = CustomizeMouth + 1; + public const int CustomizeLipColor = CustomizeLipstick + 1; + public const int CustomizeMuscleMass = CustomizeLipColor + 1; + public const int CustomizeTailShape = CustomizeMuscleMass + 1; + public const int CustomizeBustSize = CustomizeTailShape + 1; + public const int CustomizeFacePaint = CustomizeBustSize + 1; + public const int CustomizeFacePaintReversed = CustomizeFacePaint + 1; + public const int CustomizeFacePaintColor = CustomizeFacePaintReversed + 1; + + public const int MetaWetness = CustomizeFacePaintColor + 1; + public const int MetaHatState = MetaWetness + 1; + public const int MetaVisorState = MetaHatState + 1; + public const int MetaWeaponState = MetaVisorState + 1; + public const int MetaModelId = MetaWeaponState + 1; + + public const int CrestHead = MetaModelId + 1; + public const int CrestBody = CrestHead + 1; + public const int CrestOffhand = CrestBody + 1; + + public const int ParamSkinDiffuse = CrestOffhand + 1; + public const int ParamMuscleTone = ParamSkinDiffuse + 1; + public const int ParamSkinSpecular = ParamMuscleTone + 1; + public const int ParamLipDiffuse = ParamSkinSpecular + 1; + public const int ParamHairDiffuse = ParamLipDiffuse + 1; + public const int ParamHairSpecular = ParamHairDiffuse + 1; + public const int ParamHairHighlight = ParamHairSpecular + 1; + public const int ParamLeftEye = ParamHairHighlight + 1; + public const int ParamRightEye = ParamLeftEye + 1; + public const int ParamFeatureColor = ParamRightEye + 1; + public const int ParamFacePaintUvMultiplier = ParamFeatureColor + 1; + public const int ParamFacePaintUvOffset = ParamFacePaintUvMultiplier + 1; + public const int ParamDecalColor = ParamFacePaintUvOffset + 1; + + public const int Size = ParamDecalColor + 1; + + public IEnumerable All + => Enumerable.Range(0, Size - 1).Select(i => new StateIndex(i)); + + public bool GetApply(DesignBase data) + => GetFlag() switch + { + EquipFlag e => data.ApplyEquip.HasFlag(e), + CustomizeFlag c => data.ApplyCustomize.HasFlag(c), + MetaFlag m => data.ApplyMeta.HasFlag(m), + CrestFlag c => data.ApplyCrest.HasFlag(c), + CustomizeParameterFlag c => data.ApplyParameters.HasFlag(c), + bool v => v, + _ => false, + }; + + public string ToName() + => GetFlag() switch + { + EquipFlag e => GetName(e), + CustomizeFlag c => c.ToIndex().ToDefaultName(), + MetaFlag m => m.ToIndex().ToName(), + CrestFlag c => c.ToLabel(), + CustomizeParameterFlag c => c.ToName(), + bool v => "Model ID", + _ => "Unknown", + }; + + public object GetFlag() + => Value switch + { + EquipHead => EquipFlag.Head, + EquipBody => EquipFlag.Body, + EquipHands => EquipFlag.Hands, + EquipLegs => EquipFlag.Legs, + EquipFeet => EquipFlag.Feet, + EquipEars => EquipFlag.Ears, + EquipNeck => EquipFlag.Neck, + EquipWrist => EquipFlag.Wrist, + EquipRFinger => EquipFlag.RFinger, + EquipLFinger => EquipFlag.LFinger, + EquipMainhand => EquipFlag.Mainhand, + EquipOffhand => EquipFlag.Offhand, + + StainHead => EquipFlag.HeadStain, + StainBody => EquipFlag.BodyStain, + StainHands => EquipFlag.HandsStain, + StainLegs => EquipFlag.LegsStain, + StainFeet => EquipFlag.FeetStain, + StainEars => EquipFlag.EarsStain, + StainNeck => EquipFlag.NeckStain, + StainWrist => EquipFlag.WristStain, + StainRFinger => EquipFlag.RFingerStain, + StainLFinger => EquipFlag.LFingerStain, + StainMainhand => EquipFlag.MainhandStain, + StainOffhand => EquipFlag.OffhandStain, + + CustomizeRace => CustomizeFlag.Race, + CustomizeGender => CustomizeFlag.Gender, + CustomizeBodyType => CustomizeFlag.BodyType, + CustomizeHeight => CustomizeFlag.Height, + CustomizeClan => CustomizeFlag.Clan, + CustomizeFace => CustomizeFlag.Face, + CustomizeHairstyle => CustomizeFlag.Hairstyle, + CustomizeHighlights => CustomizeFlag.Highlights, + CustomizeSkinColor => CustomizeFlag.SkinColor, + CustomizeEyeColorRight => CustomizeFlag.EyeColorRight, + CustomizeHairColor => CustomizeFlag.HairColor, + CustomizeHighlightsColor => CustomizeFlag.HighlightsColor, + CustomizeFacialFeature1 => CustomizeFlag.FacialFeature1, + CustomizeFacialFeature2 => CustomizeFlag.FacialFeature2, + CustomizeFacialFeature3 => CustomizeFlag.FacialFeature3, + CustomizeFacialFeature4 => CustomizeFlag.FacialFeature4, + CustomizeFacialFeature5 => CustomizeFlag.FacialFeature5, + CustomizeFacialFeature6 => CustomizeFlag.FacialFeature6, + CustomizeFacialFeature7 => CustomizeFlag.FacialFeature7, + CustomizeLegacyTattoo => CustomizeFlag.LegacyTattoo, + CustomizeTattooColor => CustomizeFlag.TattooColor, + CustomizeEyebrows => CustomizeFlag.Eyebrows, + CustomizeEyeColorLeft => CustomizeFlag.EyeColorLeft, + CustomizeEyeShape => CustomizeFlag.EyeShape, + CustomizeSmallIris => CustomizeFlag.SmallIris, + CustomizeNose => CustomizeFlag.Nose, + CustomizeJaw => CustomizeFlag.Jaw, + CustomizeMouth => CustomizeFlag.Mouth, + CustomizeLipstick => CustomizeFlag.Lipstick, + CustomizeLipColor => CustomizeFlag.LipColor, + CustomizeMuscleMass => CustomizeFlag.MuscleMass, + CustomizeTailShape => CustomizeFlag.TailShape, + CustomizeBustSize => CustomizeFlag.BustSize, + CustomizeFacePaint => CustomizeFlag.FacePaint, + CustomizeFacePaintReversed => CustomizeFlag.FacePaintReversed, + CustomizeFacePaintColor => CustomizeFlag.FacePaintColor, + + MetaWetness => MetaFlag.Wetness, + MetaHatState => MetaFlag.HatState, + MetaVisorState => MetaFlag.VisorState, + MetaWeaponState => MetaFlag.WeaponState, + MetaModelId => true, + + CrestHead => CrestFlag.Head, + CrestBody => CrestFlag.Body, + CrestOffhand => CrestFlag.OffHand, + + ParamSkinDiffuse => CustomizeParameterFlag.SkinDiffuse, + ParamMuscleTone => CustomizeParameterFlag.MuscleTone, + ParamSkinSpecular => CustomizeParameterFlag.SkinSpecular, + ParamLipDiffuse => CustomizeParameterFlag.LipDiffuse, + ParamHairDiffuse => CustomizeParameterFlag.HairDiffuse, + ParamHairSpecular => CustomizeParameterFlag.HairSpecular, + ParamHairHighlight => CustomizeParameterFlag.HairHighlight, + ParamLeftEye => CustomizeParameterFlag.LeftEye, + ParamRightEye => CustomizeParameterFlag.RightEye, + ParamFeatureColor => CustomizeParameterFlag.FeatureColor, + ParamFacePaintUvMultiplier => CustomizeParameterFlag.FacePaintUvMultiplier, + ParamFacePaintUvOffset => CustomizeParameterFlag.FacePaintUvOffset, + ParamDecalColor => CustomizeParameterFlag.DecalColor, + + _ => -1, + }; + + public object? GetValue(in DesignData data) + { + return Value switch + { + EquipHead => data.Item(EquipSlot.Head), + EquipBody => data.Item(EquipSlot.Body), + EquipHands => data.Item(EquipSlot.Hands), + EquipLegs => data.Item(EquipSlot.Legs), + EquipFeet => data.Item(EquipSlot.Feet), + EquipEars => data.Item(EquipSlot.Ears), + EquipNeck => data.Item(EquipSlot.Neck), + EquipWrist => data.Item(EquipSlot.Wrists), + EquipRFinger => data.Item(EquipSlot.RFinger), + EquipLFinger => data.Item(EquipSlot.LFinger), + EquipMainhand => data.Item(EquipSlot.MainHand), + EquipOffhand => data.Item(EquipSlot.OffHand), + + StainHead => data.Stain(EquipSlot.Head), + StainBody => data.Stain(EquipSlot.Body), + StainHands => data.Stain(EquipSlot.Hands), + StainLegs => data.Stain(EquipSlot.Legs), + StainFeet => data.Stain(EquipSlot.Feet), + StainEars => data.Stain(EquipSlot.Ears), + StainNeck => data.Stain(EquipSlot.Neck), + StainWrist => data.Stain(EquipSlot.Wrists), + StainRFinger => data.Stain(EquipSlot.RFinger), + StainLFinger => data.Stain(EquipSlot.LFinger), + StainMainhand => data.Stain(EquipSlot.MainHand), + StainOffhand => data.Stain(EquipSlot.OffHand), + + CustomizeRace => data.Customize[CustomizeIndex.Race], + CustomizeGender => data.Customize[CustomizeIndex.Gender], + CustomizeBodyType => data.Customize[CustomizeIndex.BodyType], + CustomizeHeight => data.Customize[CustomizeIndex.Height], + CustomizeClan => data.Customize[CustomizeIndex.Clan], + CustomizeFace => data.Customize[CustomizeIndex.Face], + CustomizeHairstyle => data.Customize[CustomizeIndex.Hairstyle], + CustomizeHighlights => data.Customize[CustomizeIndex.Highlights], + CustomizeSkinColor => data.Customize[CustomizeIndex.SkinColor], + CustomizeEyeColorRight => data.Customize[CustomizeIndex.EyeColorRight], + CustomizeHairColor => data.Customize[CustomizeIndex.HairColor], + CustomizeHighlightsColor => data.Customize[CustomizeIndex.HighlightsColor], + CustomizeFacialFeature1 => data.Customize[CustomizeIndex.FacialFeature1], + CustomizeFacialFeature2 => data.Customize[CustomizeIndex.FacialFeature2], + CustomizeFacialFeature3 => data.Customize[CustomizeIndex.FacialFeature3], + CustomizeFacialFeature4 => data.Customize[CustomizeIndex.FacialFeature4], + CustomizeFacialFeature5 => data.Customize[CustomizeIndex.FacialFeature5], + CustomizeFacialFeature6 => data.Customize[CustomizeIndex.FacialFeature6], + CustomizeFacialFeature7 => data.Customize[CustomizeIndex.FacialFeature7], + CustomizeLegacyTattoo => data.Customize[CustomizeIndex.LegacyTattoo], + CustomizeTattooColor => data.Customize[CustomizeIndex.TattooColor], + CustomizeEyebrows => data.Customize[CustomizeIndex.Eyebrows], + CustomizeEyeColorLeft => data.Customize[CustomizeIndex.EyeColorLeft], + CustomizeEyeShape => data.Customize[CustomizeIndex.EyeShape], + CustomizeSmallIris => data.Customize[CustomizeIndex.SmallIris], + CustomizeNose => data.Customize[CustomizeIndex.Nose], + CustomizeJaw => data.Customize[CustomizeIndex.Jaw], + CustomizeMouth => data.Customize[CustomizeIndex.Mouth], + CustomizeLipstick => data.Customize[CustomizeIndex.Lipstick], + CustomizeLipColor => data.Customize[CustomizeIndex.LipColor], + CustomizeMuscleMass => data.Customize[CustomizeIndex.MuscleMass], + CustomizeTailShape => data.Customize[CustomizeIndex.TailShape], + CustomizeBustSize => data.Customize[CustomizeIndex.BustSize], + CustomizeFacePaint => data.Customize[CustomizeIndex.FacePaint], + CustomizeFacePaintReversed => data.Customize[CustomizeIndex.FacePaintReversed], + CustomizeFacePaintColor => data.Customize[CustomizeIndex.FacePaintColor], + + MetaWetness => data.GetMeta(MetaIndex.Wetness), + MetaHatState => data.GetMeta(MetaIndex.HatState), + MetaVisorState => data.GetMeta(MetaIndex.VisorState), + MetaWeaponState => data.GetMeta(MetaIndex.WeaponState), + MetaModelId => data.ModelId, + + CrestHead => data.Crest(CrestFlag.Head), + CrestBody => data.Crest(CrestFlag.Body), + CrestOffhand => data.Crest(CrestFlag.OffHand), + + ParamSkinDiffuse => data.Parameters[CustomizeParameterFlag.SkinDiffuse], + ParamMuscleTone => data.Parameters[CustomizeParameterFlag.MuscleTone], + ParamSkinSpecular => data.Parameters[CustomizeParameterFlag.SkinSpecular], + ParamLipDiffuse => data.Parameters[CustomizeParameterFlag.LipDiffuse], + ParamHairDiffuse => data.Parameters[CustomizeParameterFlag.HairDiffuse], + ParamHairSpecular => data.Parameters[CustomizeParameterFlag.HairSpecular], + ParamHairHighlight => data.Parameters[CustomizeParameterFlag.HairHighlight], + ParamLeftEye => data.Parameters[CustomizeParameterFlag.LeftEye], + ParamRightEye => data.Parameters[CustomizeParameterFlag.RightEye], + ParamFeatureColor => data.Parameters[CustomizeParameterFlag.FeatureColor], + ParamFacePaintUvMultiplier => data.Parameters[CustomizeParameterFlag.FacePaintUvMultiplier], + ParamFacePaintUvOffset => data.Parameters[CustomizeParameterFlag.FacePaintUvOffset], + ParamDecalColor => data.Parameters[CustomizeParameterFlag.DecalColor], + + _ => null, + }; + } + + private static string GetName(EquipFlag flag) + { + var slot = flag.ToSlot(out var stain); + return stain ? $"{slot.ToName()} Stain" : slot.ToName(); + } +} + +public static class StateExtensions +{ + public static StateIndex ToState(this EquipSlot slot, bool stain = false) + => (slot, stain) switch + { + (EquipSlot.Head, true) => new StateIndex(StateIndex.EquipHead), + (EquipSlot.Body, true) => new StateIndex(StateIndex.EquipBody), + (EquipSlot.Hands, true) => new StateIndex(StateIndex.EquipHands), + (EquipSlot.Legs, true) => new StateIndex(StateIndex.EquipLegs), + (EquipSlot.Feet, true) => new StateIndex(StateIndex.EquipFeet), + (EquipSlot.Ears, true) => new StateIndex(StateIndex.EquipEars), + (EquipSlot.Neck, true) => new StateIndex(StateIndex.EquipNeck), + (EquipSlot.Wrists, true) => new StateIndex(StateIndex.EquipWrist), + (EquipSlot.RFinger, true) => new StateIndex(StateIndex.EquipRFinger), + (EquipSlot.LFinger, true) => new StateIndex(StateIndex.EquipLFinger), + (EquipSlot.MainHand, true) => new StateIndex(StateIndex.EquipMainhand), + (EquipSlot.OffHand, true) => new StateIndex(StateIndex.EquipOffhand), + (EquipSlot.Head, false) => new StateIndex(StateIndex.StainHead), + (EquipSlot.Body, false) => new StateIndex(StateIndex.StainBody), + (EquipSlot.Hands, false) => new StateIndex(StateIndex.StainHands), + (EquipSlot.Legs, false) => new StateIndex(StateIndex.StainLegs), + (EquipSlot.Feet, false) => new StateIndex(StateIndex.StainFeet), + (EquipSlot.Ears, false) => new StateIndex(StateIndex.StainEars), + (EquipSlot.Neck, false) => new StateIndex(StateIndex.StainNeck), + (EquipSlot.Wrists, false) => new StateIndex(StateIndex.StainWrist), + (EquipSlot.RFinger, false) => new StateIndex(StateIndex.StainRFinger), + (EquipSlot.LFinger, false) => new StateIndex(StateIndex.StainLFinger), + (EquipSlot.MainHand, false) => new StateIndex(StateIndex.StainMainhand), + (EquipSlot.OffHand, false) => new StateIndex(StateIndex.StainOffhand), + _ => StateIndex.Invalid, + }; +} diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index b90d835..1833660 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -11,6 +11,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Glamourer.GameData; using Penumbra.GameData.DataContainers; +using Glamourer.Designs; namespace Glamourer.State; @@ -138,7 +139,7 @@ public class StateListener : IDisposable ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); } - private unsafe void OnCustomizeChange(Model model, ref CustomizeArray customize) + private void OnCustomizeChange(Model model, ref CustomizeArray customize) { if (!model.IsHuman) return; @@ -163,21 +164,21 @@ public class StateListener : IDisposable var model = state.ModelData.Customize; if (customize.Gender != model.Gender || customize.Clan != model.Clan) { - _manager.ChangeCustomize(state, in customize, CustomizeFlagExtensions.AllRelevant, StateChanged.Source.Game); + _manager.ChangeEntireCustomize(state, in customize, CustomizeFlagExtensions.All, ApplySettings.Game); return; } var set = _customizations.Manager.GetSet(model.Clan, model.Gender); foreach (var index in CustomizationExtensions.AllBasic) { - if (state[index] is not StateChanged.Source.Fixed) + if (state.Sources[index] is not StateSource.Fixed) { var newValue = customize[index]; var oldValue = model[index]; if (newValue != oldValue) { if (set.Validate(index, newValue, out _, model.Face)) - _manager.ChangeCustomize(state, index, newValue, StateChanged.Source.Game); + _manager.ChangeCustomize(state, index, newValue, ApplySettings.Game); else customize[index] = oldValue; } @@ -213,7 +214,7 @@ public class StateListener : IDisposable && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor); - locked = state[slot, false] is StateChanged.Source.Ipc; + locked = state.Sources[slot, false] is StateSource.Ipc; } _funModule.ApplyFunToSlot(actor, ref armor, slot); @@ -240,10 +241,10 @@ public class StateListener : IDisposable 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.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) { - _manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game); - _manager.ChangeStain(state, slot, current.Stain, StateChanged.Source.Game); + _manager.ChangeItem(state, slot, currentItem, ApplySettings.Game); + _manager.ChangeStain(state, slot, current.Stain, ApplySettings.Game); switch (slot) { case EquipSlot.MainHand: @@ -251,7 +252,7 @@ public class StateListener : IDisposable _applier.ChangeWeapon(objects, slot, currentItem, stain); break; default: - _applier.ChangeArmor(objects, slot, current.ToArmor(), state[slot, false] is not StateChanged.Source.Ipc, + _applier.ChangeArmor(objects, slot, current.ToArmor(), state.Sources[slot, false] is not StateSource.Ipc, state.ModelData.IsHatVisible()); break; } @@ -285,13 +286,13 @@ 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) - _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); + if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) + _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); + if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc) + _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; break; @@ -384,13 +385,13 @@ 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) - _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); + if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) + _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); + if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc) + _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; @@ -418,8 +419,8 @@ public class StateListener : IDisposable switch (UpdateBaseCrest(actor, state, slot, value)) { case UpdateState.Change: - if (state[slot] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game); + if (state.Sources[slot] is not StateSource.Fixed and not StateSource.Ipc) + _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), ApplySettings.Game); else value = state.ModelData.Crest(slot); break; @@ -564,10 +565,10 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) + if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed or StateSource.Ipc) value = state.ModelData.IsVisorToggled(); else - _manager.ChangeVisorState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.VisorState, value, ApplySettings.Game); } else { @@ -597,10 +598,10 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) + if (state.Sources[MetaIndex.HatState] is StateSource.Fixed or StateSource.Ipc) value = state.ModelData.IsHatVisible(); else - _manager.ChangeHatState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.HatState, value, ApplySettings.Game); } else { @@ -630,10 +631,10 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) + if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed or StateSource.Ipc) value = state.ModelData.IsWeaponVisible(); else - _manager.ChangeWeaponState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.WeaponState, value, ApplySettings.Game); } else { @@ -699,9 +700,9 @@ public class StateListener : IDisposable return; var data = new ActorData(gameObject, _creatingIdentifier.ToName()); - _applier.ChangeHatState(data, _creatingState.ModelData.IsHatVisible()); - _applier.ChangeWeaponState(data, _creatingState.ModelData.IsWeaponVisible()); - _applier.ChangeWetness(data, _creatingState.ModelData.IsWet()); + _applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible()); + _applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet()); + _applier.ChangeMetaState(data, MetaIndex.WeaponState, _creatingState.ModelData.IsWeaponVisible()); ApplyParameters(_creatingState, drawObject); } @@ -723,7 +724,7 @@ public class StateListener : IDisposable _customizeState = null; } - private unsafe void ApplyParameters(ActorState state, Model model) + private void ApplyParameters(ActorState state, Model model) { if (!model.IsHuman) return; @@ -732,30 +733,30 @@ public class StateListener : IDisposable foreach (var flag in CustomizeParameterExtensions.AllFlags) { var newValue = data[flag]; - switch (state[flag]) + switch (state.Sources[flag]) { - case StateChanged.Source.Game: + case StateSource.Game: if (state.BaseData.Parameters.Set(flag, newValue)) - _manager.ChangeCustomizeParameter(state, flag, newValue, StateChanged.Source.Game); + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); break; - case StateChanged.Source.Manual: + case StateSource.Manual: if (state.BaseData.Parameters.Set(flag, newValue)) - _manager.ChangeCustomizeParameter(state, flag, newValue, StateChanged.Source.Game); + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); else if (_config.UseAdvancedParameters) model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; - case StateChanged.Source.Fixed: + case StateSource.Fixed: state.BaseData.Parameters.Set(flag, newValue); if (_config.UseAdvancedParameters) model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; - case StateChanged.Source.Ipc: + case StateSource.Ipc: state.BaseData.Parameters.Set(flag, newValue); model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; - case StateChanged.Source.Pending: + case StateSource.Pending: state.BaseData.Parameters.Set(flag, newValue); - state[flag] = StateChanged.Source.Manual; + state.Sources[flag] = StateSource.Manual; if (_config.UseAdvancedParameters) model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 0286e73..2643271 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Shader; using Glamourer.Designs; +using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; using Glamourer.Interop; @@ -13,16 +13,18 @@ using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateManager( +public sealed class StateManager( ActorManager _actors, - ItemManager _items, - StateChanged _event, - StateApplier _applier, - StateEditor _editor, + ItemManager items, + StateChanged @event, + StateApplier applier, + InternalStateEditor editor, HumanModelList _humans, - ICondition _condition, - IClientState _clientState) - : IReadOnlyDictionary + IClientState _clientState, + Configuration config, + JobChangeState jobChange, + DesignMerger merger) + : StateEditor(editor, applier, @event, jobChange, config, items, merger), IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -93,7 +95,7 @@ public class StateManager( // If the given actor is not a character, just return a default character. if (!actor.IsCharacter) { - ret.SetDefaultEquipment(_items); + ret.SetDefaultEquipment(Items); return ret; } @@ -127,7 +129,7 @@ public class StateManager( // 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 headItem = Items.Identify(EquipSlot.Head, head.Set, head.Variant); ret.SetItem(EquipSlot.Head, headItem); ret.SetStain(EquipSlot.Head, head.Stain); @@ -135,7 +137,7 @@ public class StateManager( foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) { var armor = model.GetArmor(slot); - var item = _items.Identify(slot, armor.Set, armor.Variant); + var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); ret.SetStain(slot, armor.Stain); } @@ -157,7 +159,7 @@ public class StateManager( foreach (var slot in EquipSlotExtensions.EqdpSlots) { var armor = actor.GetArmor(slot); - var item = _items.Identify(slot, armor.Set, armor.Variant); + var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); ret.SetStain(slot, armor.Stain); } @@ -172,8 +174,8 @@ public class StateManager( } // Set the weapons regardless of source. - var mainItem = _items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); - var offItem = _items.Identify(EquipSlot.OffHand, off.Skeleton, off.Weapon, off.Variant, mainItem.Type); + var mainItem = Items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); + var offItem = Items.Identify(EquipSlot.OffHand, off.Skeleton, off.Weapon, off.Variant, mainItem.Type); ret.SetItem(EquipSlot.MainHand, mainItem); ret.SetStain(EquipSlot.MainHand, main.Stain); ret.SetItem(EquipSlot.OffHand, offItem); @@ -197,7 +199,7 @@ public class StateManager( if (mainhand.Skeleton.Id is < 1601 or >= 1651) return; - var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Skeleton, (Variant)offhand.Weapon.Id); + var gauntlets = Items.Identify(EquipSlot.Hands, offhand.Skeleton, (Variant)offhand.Weapon.Id); offhand.Skeleton = (PrimaryId)(mainhand.Skeleton.Id + 50); offhand.Variant = mainhand.Variant; offhand.Weapon = mainhand.Weapon; @@ -205,279 +207,11 @@ public class StateManager( ret.SetStain(EquipSlot.Hands, mainhand.Stain); } - #region Change Values - /// Turn an actor human. - public void TurnHuman(ActorState state, StateChanged.Source source, uint key = 0) + public void TurnHuman(ActorState state, StateSource source, uint key = 0) => ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); - /// Turn an actor to. - public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateChanged.Source source, - uint key = 0) - { - if (!_editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) - return; - - var actors = _applier.ForceRedraw(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId)); - } - - /// Change a customization value. - public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeCustomize(state, idx, value, source, out var old, key)) - return; - - var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Customize, source, state, actors, (old, value, idx)); - } - - /// Change an entire customization array according to flags. - public void ChangeCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag apply, StateChanged.Source source, - uint key = 0) - { - if (!_editor.ChangeHumanCustomize(state, customizeInput, apply, source, out var old, out var applied, key)) - return; - - var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.EntireCustomize, source, state, actors, (old, applied)); - } - - /// Change a single piece of equipment without stain. - /// 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) - { - if (!_editor.ChangeItem(state, slot, item, source, out var old, 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)); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); - _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) - { - if (!_editor.ChangeEquip(state, slot, item, stain, source, out var old, out var oldStain, 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)); - 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)); - } - - /// Change only the stain of an equipment piece. - /// Do not use this in the same frame as ChangeEquip, use instead. - public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeStain(state, slot, stain, source, out var old, key)) - return; - - var actors = _applier.ChangeStain(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot)); - } - - /// Change the crest of an equipment piece. - public void ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeCrest(state, slot, crest, source, out var old, key)) - return; - - var actors = _applier.ChangeCrests(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Crest, source, state, actors, (old, crest, slot)); - } - - /// Change the crest of an equipment piece. - public void ChangeCustomizeParameter(ActorState state, CustomizeParameterFlag flag, CustomizeParameterValue value, - StateChanged.Source source, uint key = 0) - { - // Also apply main color to highlights when highlights is off. - if (!state.ModelData.Customize.Highlights && flag is CustomizeParameterFlag.HairDiffuse) - ChangeCustomizeParameter(state, CustomizeParameterFlag.HairHighlight, value, source, key); - - if (!_editor.ChangeParameter(state, flag, value, source, out var old, key)) - return; - - var @new = state.ModelData.Parameters[flag]; - var actors = _applier.ChangeParameters(state, flag, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {flag} crest in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Parameter, source, state, actors, (old, @new, flag)); - } - - /// Change hat visibility. - public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeHatState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.HatState)); - } - - /// Change weapon visibility. - public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeWeaponState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Weapon Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.WeaponState)); - } - - /// Change visor state. - public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeVisor(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Visor State in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.VisorState)); - } - - /// Set GPose Wetness. - public void ChangeWetness(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, value, source, out var old, key)) - return; - - var actors = _applier.ChangeWetness(state, true); - Glamourer.Log.Verbose( - $"Set Wetness in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaIndex.Wetness], state, actors, (old, value, ActorState.MetaIndex.Wetness)); - } - - #endregion - - public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.GetDesignDataRef().GetEquipmentPtr(), - source, - out var oldModelId, key)) - return; - - var redraw = oldModelId != design.DesignData.ModelId || !design.DesignData.IsHuman; - if (design.DoApplyWetness()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, design.DesignData.IsWet(), source, out _, key); - - if (state.ModelData.IsHuman) - { - if (design.DoApplyHatVisible()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, design.DesignData.IsHatVisible(), source, out _, key); - if (design.DoApplyWeaponVisible()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, design.DesignData.IsWeaponVisible(), source, out _, key); - if (design.DoApplyVisorToggle()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, design.DesignData.IsVisorToggled(), source, out _, key); - - var flags = state.AllowsRedraw(_condition) - ? design.ApplyCustomize - : design.ApplyCustomize & ~CustomizeFlagExtensions.RedrawRequired; - _editor.ChangeHumanCustomize(state, design.DesignData.Customize, flags, source, out _, out var applied, key); - redraw |= applied.RequiresRedraw(); - - foreach (var slot in EquipSlotExtensions.FullSlots) - HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); - - foreach (var slot in CrestExtensions.AllRelevantSet.Where(design.DoApplyCrest)) - _editor.ChangeCrest(state, slot, design.DesignData.Crest(slot), source, out _, key); - - var paramSource = source is StateChanged.Source.Manual - ? StateChanged.Source.Pending - : source; - - foreach (var flag in CustomizeParameterExtensions.AllFlags.Where(design.DoApplyParameter)) - _editor.ChangeParameter(state, flag, design.DesignData.Parameters[flag], paramSource, out _, key); - - // Do not apply highlights from a design if highlights is unchecked. - if (!state.ModelData.Customize.Highlights) - _editor.ChangeParameter(state, CustomizeParameterFlag.HairHighlight, - state.ModelData.Parameters[CustomizeParameterFlag.HairDiffuse], - state[CustomizeParameterFlag.HairDiffuse], out _, key); - } - - var actors = ApplyAll(state, redraw, false); - Glamourer.Log.Verbose( - $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, design); - return; - - void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) - { - var unused = (applyPiece, applyStain) switch - { - (false, false) => false, - (true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key), - (false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key), - (true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _, - out _, key), - }; - } - } - - private ActorData ApplyAll(ActorState state, bool redraw, bool withLock) - { - var actors = _applier.ChangeWetness(state, true); - if (redraw) - { - if (withLock) - state.TempLock(); - _applier.ForceRedraw(actors); - } - else - { - _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, - 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)); - var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; - _applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); - } - - if (state.ModelData.IsHuman) - { - _applier.ChangeHatState(actors, state.ModelData.IsHatVisible()); - _applier.ChangeWeaponState(actors, state.ModelData.IsWeaponVisible()); - _applier.ChangeVisor(actors, state.ModelData.IsVisorToggled()); - _applier.ChangeCrests(actors, state.ModelData.CrestVisibility); - _applier.ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); - } - - return actors; - } - - public void ResetState(ActorState state, StateChanged.Source source, uint key = 0) + public void ResetState(ActorState state, StateSource source, uint key = 0) { if (!state.Unlock(key)) return; @@ -489,33 +223,33 @@ public class StateManager( state.ModelData = state.BaseData; state.ModelData.SetIsWet(false); foreach (var index in Enum.GetValues()) - state[index] = StateChanged.Source.Game; + state.Sources[index] = StateSource.Game; foreach (var slot in EquipSlotExtensions.FullSlots) { - state[slot, true] = StateChanged.Source.Game; - state[slot, false] = StateChanged.Source.Game; + state.Sources[slot, true] = StateSource.Game; + state.Sources[slot, false] = StateSource.Game; } - foreach (var type in Enum.GetValues()) - state[type] = StateChanged.Source.Game; + foreach (var type in Enum.GetValues()) + state.Sources[type] = StateSource.Game; foreach (var slot in CrestExtensions.AllRelevantSet) - state[slot] = StateChanged.Source.Game; + state.Sources[slot] = StateSource.Game; foreach (var flag in CustomizeParameterExtensions.AllFlags) - state[flag] = StateChanged.Source.Game; + state.Sources[flag] = StateSource.Game; var actors = ActorData.Invalid; - if (source is StateChanged.Source.Manual or StateChanged.Source.Ipc) - actors = ApplyAll(state, redraw, true); + if (source is StateSource.Manual or StateSource.Ipc) + actors = Applier.ApplyAll(state, redraw, true); Glamourer.Log.Verbose( $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Reset, source, state, actors, null); + StateChanged.Invoke(StateChanged.Type.Reset, source, state, actors, null); } - public void ResetAdvancedState(ActorState state, StateChanged.Source source, uint key = 0) + public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) { if (!state.Unlock(key) || !state.ModelData.IsHuman) return; @@ -523,14 +257,14 @@ public class StateManager( state.ModelData.Parameters = state.BaseData.Parameters; foreach (var flag in CustomizeParameterExtensions.AllFlags) - state[flag] = StateChanged.Source.Game; + state.Sources[flag] = StateSource.Game; var actors = ActorData.Invalid; - if (source is StateChanged.Source.Manual or StateChanged.Source.Ipc) - actors = _applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); + if (source is StateSource.Manual or StateSource.Ipc) + actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); Glamourer.Log.Verbose( $"Reset advanced customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Reset, source, state, actors, null); + StateChanged.Invoke(StateChanged.Type.Reset, source, state, actors, null); } public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) @@ -538,69 +272,69 @@ public class StateManager( if (!state.Unlock(key)) return; - foreach (var index in Enum.GetValues().Where(i => state[i] is StateChanged.Source.Fixed)) + foreach (var index in Enum.GetValues().Where(i => state.Sources[i] is StateSource.Fixed)) { - state[index] = StateChanged.Source.Game; + state.Sources[index] = StateSource.Game; state.ModelData.Customize[index] = state.BaseData.Customize[index]; } foreach (var slot in EquipSlotExtensions.FullSlots) { - if (state[slot, true] is StateChanged.Source.Fixed) + if (state.Sources[slot, true] is StateSource.Fixed) { - state[slot, true] = StateChanged.Source.Game; + state.Sources[slot, true] = StateSource.Game; state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); } - if (state[slot, false] is StateChanged.Source.Fixed) + if (state.Sources[slot, false] is StateSource.Fixed) { - state[slot, false] = StateChanged.Source.Game; + state.Sources[slot, false] = StateSource.Game; state.ModelData.SetItem(slot, state.BaseData.Item(slot)); } } foreach (var slot in CrestExtensions.AllRelevantSet) { - if (state[slot] is StateChanged.Source.Fixed) + if (state.Sources[slot] is StateSource.Fixed) { - state[slot] = StateChanged.Source.Game; + state.Sources[slot] = StateSource.Game; state.ModelData.SetCrest(slot, state.BaseData.Crest(slot)); } } foreach (var flag in CustomizeParameterExtensions.AllFlags) { - switch (state[flag]) + switch (state.Sources[flag]) { - case StateChanged.Source.Fixed: - case StateChanged.Source.Manual when !respectManualPalettes: - state[flag] = StateChanged.Source.Game; + case StateSource.Fixed: + case StateSource.Manual when !respectManualPalettes: + state.Sources[flag] = StateSource.Game; state.ModelData.Parameters[flag] = state.BaseData.Parameters[flag]; break; } } - if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed) + if (state.Sources[MetaIndex.HatState] is StateSource.Fixed) { - state[ActorState.MetaIndex.HatState] = StateChanged.Source.Game; + state.Sources[MetaIndex.HatState] = StateSource.Game; state.ModelData.SetHatVisible(state.BaseData.IsHatVisible()); } - if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed) + if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed) { - state[ActorState.MetaIndex.VisorState] = StateChanged.Source.Game; + state.Sources[MetaIndex.VisorState] = StateSource.Game; state.ModelData.SetVisor(state.BaseData.IsVisorToggled()); } - if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed) + if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed) { - state[ActorState.MetaIndex.WeaponState] = StateChanged.Source.Game; + state.Sources[MetaIndex.WeaponState] = StateSource.Game; state.ModelData.SetWeaponVisible(state.BaseData.IsWeaponVisible()); } - if (state[ActorState.MetaIndex.Wetness] is StateChanged.Source.Fixed) + if (state.Sources[MetaIndex.Wetness] is StateSource.Fixed) { - state[ActorState.MetaIndex.Wetness] = StateChanged.Source.Game; + state.Sources[MetaIndex.Wetness] = StateSource.Game; state.ModelData.SetIsWet(state.BaseData.IsWet()); } } @@ -610,7 +344,8 @@ public class StateManager( if (!GetOrCreate(actor, out var state)) return; - ApplyAll(state, !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), + Applier.ApplyAll(state, + !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); } diff --git a/Glamourer/State/StateSource.cs b/Glamourer/State/StateSource.cs new file mode 100644 index 0000000..1d66a46 --- /dev/null +++ b/Glamourer/State/StateSource.cs @@ -0,0 +1,75 @@ +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public enum StateSource : byte +{ + Game, + Manual, + Fixed, + Ipc, + + // Only used for CustomizeParameters. + Pending, +} + +public unsafe struct StateSources +{ + public const int Size = (StateIndex.Size + 1) / 2; + private fixed byte _data[Size]; + + + public StateSources() + { } + + public StateSource this[StateIndex index] + { + get + { + var val = _data[index.Value / 2]; + return (StateSource)((index.Value & 1) == 1 ? val >> 4 : val & 0x0F); + } + set + { + var val = _data[index.Value / 2]; + if ((index.Value & 1) == 1) + val = (byte)((val & 0x0F) | ((byte)value << 4)); + else + val = (byte)((val & 0xF0) | (byte)value); + _data[index.Value / 2] = val; + } + } + + public StateSource this[EquipSlot slot, bool stain] + { + get => this[slot.ToState(stain)]; + set => this[slot.ToState(stain)] = value; + } + + public void RemoveFixedDesignSources() + { + for (var i = 0; i < Size; ++i) + { + var value = _data[i]; + switch (value) + { + case (byte)StateSource.Fixed | ((byte)StateSource.Fixed << 4): + _data[i] = (byte)StateSource.Manual | ((byte)StateSource.Manual << 4); + break; + + case (byte)StateSource.Game | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.Manual | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.Ipc | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.Pending | ((byte)StateSource.Fixed << 4): + _data[i] = (byte)((value & 0x0F) | ((byte)StateSource.Manual << 4)); + break; + case (byte)StateSource.Fixed: + case ((byte)StateSource.Manual << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.Ipc << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.Pending << 4) | (byte)StateSource.Fixed: + _data[i] = (byte)((value & 0xF0) | (byte)StateSource.Manual); + break; + } + } + } +} diff --git a/OtterGui b/OtterGui index d734d5d..04eb0b5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d734d5d2b0686db0f5f4270dc379360d31f72e59 +Subproject commit 04eb0b5ed3930e9cb87ad00dffa9c8be90b58bb3 diff --git a/Penumbra.GameData b/Penumbra.GameData index 1ebaf1d..260ac69 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1ebaf1d209b7edc783896b3a6af4907c91bb302e +Subproject commit 260ac69cd6f17050eaf9b7e0b5ce9a8843edfee4