diff --git a/Glamourer/Events/GPoseService.cs b/Glamourer/Events/GPoseService.cs new file mode 100644 index 0000000..358b116 --- /dev/null +++ b/Glamourer/Events/GPoseService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Concurrent; +using Dalamud.Game; +using OtterGui.Classes; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Glamourer.Events; + +public class GPoseService : EventWrapper, GPoseService.Priority> +{ + private readonly Framework _framework; + + private readonly ConcurrentQueue _onLeave = new(); + private readonly ConcurrentQueue _onEnter = new(); + + public enum Priority + { } + + public bool InGPose { get; private set; } = false; + + public GPoseService(Framework framework) + : base(nameof(GPoseService)) + { + _framework = framework; + _framework.Update += OnFramework; + } + + public new void Dispose() + { + _framework.Update -= OnFramework; + base.Dispose(); + } + + public void AddActionOnLeave(Action onLeave) + { + if (InGPose) + _onLeave.Enqueue(onLeave); + else + onLeave(); + } + + public void AddActionOnEnter(Action onEnter) + { + if (InGPose) + onEnter(); + else + _onEnter.Enqueue(onEnter); + } + + private void OnFramework(Framework _) + { + var inGPose = GameMain.IsInGPose(); + if (InGPose == inGPose) + return; + + InGPose = inGPose; + Invoke(this, InGPose); + var actions = InGPose ? _onEnter : _onLeave; + foreach (var action in actions) + { + try + { + action(); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error executing GPose action:\n{ex}"); + } + } + } +} diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs index 933cf0c..a6df40c 100644 --- a/Glamourer/Interop/Structs/ActorData.cs +++ b/Glamourer/Interop/Structs/ActorData.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using OtterGui.Log; +using Penumbra.GameData.Actors; namespace Glamourer.Interop.Structs; @@ -36,4 +37,13 @@ public readonly struct ActorData ? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString()))) : new LazyString(() => invalid); } + + private ActorData(List objects, string label) + { + Objects = objects; + Label = label; + } + + public ActorData OnlyGPose() + => new(Objects.Where(o => o.Index is >= (int)ScreenActor.GPosePlayer and < (int)ScreenActor.CutsceneEnd).ToList(), Label); } diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 82cfd3a..e0964be 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -69,7 +69,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index b0fbba3..9cfa129 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -167,12 +167,11 @@ public class StateApplier public ActorData ChangeWeapon(ActorState state, EquipSlot slot, bool apply, bool onlyGPose) { var data = GetData(state); + if (onlyGPose) + data = data.OnlyGPose(); + if (apply) - { - if (onlyGPose) - data.Objects.RemoveAll(a => a.Index is < (int)ScreenActor.GPosePlayer or >= (int)ScreenActor.CutsceneEnd); ChangeWeapon(data, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); - } return data; } @@ -187,20 +186,6 @@ public class StateApplier _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain)); } - /// - public ActorData ChangeMainhand(ActorState state, bool apply, bool onlyGPose) - { - var data = GetData(state); - if (apply) - { - if (onlyGPose) - data.Objects.RemoveAll(a => a.Index is < (int)ScreenActor.GPosePlayer or >= (int)ScreenActor.CutsceneEnd); - ChangeMainhand(data, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); - } - - return data; - } - /// Apply a weapon to the offhand. public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) { @@ -209,20 +194,6 @@ public class StateApplier _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); } - /// - public ActorData ChangeOffhand(ActorState state, bool apply, bool onlyGPose) - { - var data = GetData(state); - if (apply) - { - if (onlyGPose) - data.Objects.RemoveAll(a => a.Index is < (int)ScreenActor.GPosePlayer or >= (int)ScreenActor.CutsceneEnd); - ChangeOffhand(data, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); - } - - return data; - } - /// Change the visor state of actors only on the draw object. public void ChangeVisor(ActorData data, bool value) { diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 86650a2..12bcc2a 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -14,12 +14,14 @@ public class StateEditor private readonly ItemManager _items; private readonly CustomizationService _customizations; private readonly HumanModelList _humans; + private readonly GPoseService _gPose; - public StateEditor(CustomizationService customizations, HumanModelList humans, ItemManager items) + public StateEditor(CustomizationService customizations, HumanModelList humans, ItemManager items, GPoseService gPose) { _customizations = customizations; _humans = humans; _items = items; + _gPose = gPose; } /// Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. @@ -124,8 +126,18 @@ public class StateEditor // 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.MainhandType.ValidOffhand()) - return false; + || 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; @@ -143,8 +155,19 @@ public class StateEditor // 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.MainhandType.ValidOffhand()) - return false; + || 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); @@ -170,11 +193,12 @@ public class StateEditor { (var setter, oldValue) = index switch { - 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."), + 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."), }; if (!state.CanUnlock(key)) diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 1eacaa2..cde6f16 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -13,7 +13,6 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.String; namespace Glamourer.State; @@ -258,7 +257,8 @@ public class StateManager : IReadOnlyDictionary 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 != old.Type); + : _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)); @@ -273,7 +273,7 @@ public class StateManager : IReadOnlyDictionary 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 != old.Type); + : _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.Value} to {stain.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); _event.Invoke(type, source, state, actors, (old, item, slot)); @@ -401,8 +401,10 @@ public class StateManager : IReadOnlyDictionary _applier.ChangeCustomize(actors, state.ModelData.Customize); foreach (var slot in EquipSlotExtensions.EqdpSlots) _applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.ModelData.IsHatVisible()); - foreach (var slot in EquipSlotExtensions.WeaponSlots) - _applier.ChangeWeapon(actors, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + 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)