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/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index ee4bd13..3b3b43d 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -4,7 +4,6 @@ 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; @@ -17,6 +16,7 @@ namespace Glamourer.Designs; public class DesignManager { private readonly CustomizeService _customizations; + private readonly Configuration _config; private readonly ItemManager _items; private readonly HumanModelList _humans; private readonly SaveService _saveService; @@ -28,14 +28,15 @@ public class DesignManager => _designs; public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations, - DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader) + DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader, Configuration config) { - _designs = storage; - _saveService = saveService; - _items = items; - _customizations = customizations; - _event = @event; - _humans = humans; + _designs = storage; + _config = config; + _saveService = saveService; + _items = items; + _customizations = customizations; + _event = @event; + _humans = humans; LoadDesigns(designLinkLoader); CreateDesignFolder(saveService); @@ -382,26 +383,18 @@ public class DesignManager 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))) + 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})."); - _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); + _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff, newGauntlets)); return; case EquipSlot.OffHand: @@ -415,7 +408,7 @@ public class DesignManager _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)); + _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item, (EquipItem?)null)); return; default: return; } @@ -503,15 +496,7 @@ public class DesignManager /// Change the bool value of one of the meta flags. public void ChangeMeta(Design design, MetaIndex metaIndex, bool value) { - var change = metaIndex switch - { - MetaIndex.Wetness => design.GetDesignDataRef().SetIsWet(value), - MetaIndex.HatState => design.GetDesignDataRef().SetHatVisible(value), - MetaIndex.VisorState => design.GetDesignDataRef().SetVisor(value), - MetaIndex.WeaponState => design.GetDesignDataRef().SetWeaponVisible(value), - _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), - }; - if (!change) + if (!design.GetDesignDataRef().SetMeta(metaIndex, value)) return; design.LastEdit = DateTimeOffset.UtcNow; @@ -753,4 +738,46 @@ public class DesignManager return (actualName, path); } + + /// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. + private bool ChangeMainhandPeriphery(Design 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 (_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/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 9b75d5a..121c58c 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -59,7 +59,7 @@ public sealed class DesignChanged() /// 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)]. 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/State/StateManager.cs b/Glamourer/State/StateManager.cs index 4ccf42c..518b435 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -20,7 +20,8 @@ public class StateManager( StateEditor _editor, HumanModelList _humans, ICondition _condition, - IClientState _clientState) + IClientState _clientState, + Configuration _config) : IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -260,6 +261,10 @@ public class StateManager( ? _applier.ChangeArmor(state, slot, source is StateSource.Manual or StateSource.Ipc) : _applier.ChangeWeapon(state, slot, 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, source, key); + 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)); @@ -276,6 +281,10 @@ public class StateManager( ? _applier.ChangeArmor(state, slot, source is StateSource.Manual or StateSource.Ipc) : _applier.ChangeWeapon(state, slot, 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, source, key); + 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)); @@ -290,6 +299,7 @@ public class StateManager( return; var actors = _applier.ChangeStain(state, slot, 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")}.]"); _event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot)); @@ -430,9 +440,9 @@ public class StateManager( if (state.ModelData.IsHuman) { - _applier.ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); + _applier.ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); _applier.ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); - _applier.ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); + _applier.ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); _applier.ChangeCrests(actors, state.ModelData.CrestVisibility); _applier.ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); } @@ -503,7 +513,7 @@ public class StateManager( foreach (var index in Enum.GetValues().Where(i => state.Sources[i] is StateSource.Fixed)) { - state.Sources[index] = StateSource.Game; + state.Sources[index] = StateSource.Game; state.ModelData.Customize[index] = state.BaseData.Customize[index]; } @@ -537,7 +547,7 @@ public class StateManager( { case StateSource.Fixed: case StateSource.Manual when !respectManualPalettes: - state.Sources[flag] = StateSource.Game; + state.Sources[flag] = StateSource.Game; state.ModelData.Parameters[flag] = state.BaseData.Parameters[flag]; break; } @@ -579,4 +589,20 @@ public class StateManager( public void DeleteState(ActorIdentifier identifier) => _states.Remove(identifier); + + /// Apply offhand item and potentially gauntlets if configured. + private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, StateSource source, uint key = 0) + { + if (!_config.ChangeEntireItem || 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), source, key); + + 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), source, key); + } }