diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index d746295e..92ebbb2c 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -35,6 +35,16 @@ public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > Entry = Eqdp.Mask( Slot ) & entry; } + public EqdpManipulation Copy( EqdpManipulation entry ) + { + if( entry.Slot != Slot ) + { + var (bit1, bit2) = entry.Entry.ToBits( entry.Slot ); + return new EqdpManipulation(Eqdp.FromSlotAndBits( Slot, bit1, bit2 ), Slot, Gender, Race, SetId); + } + return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); + } + public EqdpManipulation Copy( EqdpEntry entry ) => new(entry, Slot, Gender, Race, SetId); diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index d1ec5f3a..1d4b370b 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -209,7 +209,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa { Type.Eqp => Eqp.Copy( other.Eqp.Entry ), Type.Gmp => Gmp.Copy( other.Gmp.Entry ), - Type.Eqdp => Eqdp.Copy( other.Eqdp.Entry ), + Type.Eqdp => Eqdp.Copy( other.Eqdp ), Type.Est => Est.Copy( other.Est.Entry ), Type.Rsp => Rsp.Copy( other.Rsp.Entry ), Type.Imc => Imc.Copy( other.Imc.Entry ), diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 231ce02d..22e37eba 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -33,6 +33,69 @@ public static class EquipmentSwap : Array.Empty< EquipSlot >(); } + public static Item[] CreateTypeSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, + EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo ) + { + LookupItem( itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom ); + LookupItem( itemTo, out var actualSlotTo, out var idTo, out var variantTo ); + if( actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot() ) + { + throw new ItemSwap.InvalidItemTypeException(); + } + + var ( imcFileFrom, variants, affectedItems ) = GetVariants( slotFrom, idFrom, idTo, variantFrom ); + var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default ); + var imcFileTo = new ImcFile( imcManip ); + var skipFemale = false; + var skipMale = false; + var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ) ) ).Imc.Entry.MaterialId; + foreach( var gr in Enum.GetValues< GenderRace >() ) + { + switch( gr.Split().Item1 ) + { + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; + case Gender.FemaleNpc when skipFemale: continue; + } + + if( CharacterUtility.EqdpIdx( gr, true ) < 0 ) + { + continue; + } + + try + { + var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo ); + if( eqdp != null ) + { + swaps.Add( eqdp ); + } + } + catch( ItemSwap.MissingFileException e ) + { + switch( gr ) + { + case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: + skipMale = true; + continue; + case GenderRace.MidlanderFemale when e.Type == ResourceType.Mdl: + skipFemale = true; + continue; + default: throw; + } + } + } + + foreach( var variant in variants ) + { + var imc = CreateImc( redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); + swaps.Add( imc ); + } + + return affectedItems; + } + public static Item[] CreateItemSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, Item itemTo, bool rFinger = true, bool lFinger = true ) { @@ -59,9 +122,9 @@ public static class EquipmentSwap var affectedItems = Array.Empty< Item >(); foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) { - (var imcFileFrom, var variants, affectedItems) = GetVariants( slot, idFrom, idTo, variantFrom ); + ( var imcFileFrom, var variants, affectedItems ) = GetVariants( slot, idFrom, idTo, variantFrom ); var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); - var imcFileTo = new ImcFile( imcManip); + var imcFileTo = new ImcFile( imcManip ); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -89,7 +152,7 @@ public static class EquipmentSwap continue; } - + try { var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); @@ -99,7 +162,7 @@ public static class EquipmentSwap } var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false; - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); if( est != null ) { swaps.Add( est ); @@ -132,15 +195,18 @@ public static class EquipmentSwap public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) + => CreateEqdp( redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo ); + public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo ) { var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idFrom.Value ), slot, gender, race, idFrom.Value ); - var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idTo.Value ), slot, gender, race, idTo.Value ); + var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotFrom.IsAccessory(), idFrom.Value ), slotFrom, gender, race, idFrom.Value ); + var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotTo.IsAccessory(), idTo.Value ), slotTo, gender, race, idTo.Value ); var meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slot ); + var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slotFrom ); if( ownMdl ) { - var mdl = CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo ); + var mdl = CreateMdl( redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo ); meta.ChildSwaps.Add( mdl ); } else if( !ownMtrl && meta.SwapAppliedIsDefault ) @@ -152,15 +218,17 @@ public static class EquipmentSwap } public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) + => CreateMdl( redirections, slot, slot, gr, idFrom, idTo, mtrlTo ); + + public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) { - var accessory = slot.IsAccessory(); - var mdlPathFrom = accessory ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slot ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); - var mdlPathTo = accessory ? GamePaths.Accessory.Mdl.Path( idTo, gr, slot ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); + var mdlPathFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slotFrom ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slotFrom ); + var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idTo, gr, slotTo ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slotTo ); var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) { - var mtrl = CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); + var mtrl = CreateMtrl( redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); if( mtrl != null ) { mdl.ChildSwaps.Add( mtrl ); @@ -182,22 +250,22 @@ public static class EquipmentSwap variant = ( byte )( ( Quad )i.ModelMain ).B; } - private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom ) + private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom ) { - var entry = new ImcManipulation( slot, variantFrom, idFrom.Value, default ); + var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default ); var imc = new ImcFile( entry ); Item[] items; byte[] variants; if( idFrom.Value == idTo.Value ) { - items = Penumbra.Identifier.Identify( idFrom, variantFrom, slot ).ToArray(); + items = Penumbra.Identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); variants = new[] { variantFrom }; } else { - items = Penumbra.Identifier.Identify( slot.IsEquipment() - ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) - : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); + items = Penumbra.Identifier.Identify( slotFrom.IsEquipment() + ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) + : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); } @@ -218,11 +286,15 @@ public static class EquipmentSwap public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) + => CreateImc( redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo ); + + public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) { - var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slot ), variantFrom ); - var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ); - var manipulationFrom = new ImcManipulation( slot, variantFrom, idFrom.Value, entryFrom ); - var manipulationTo = new ImcManipulation( slot, variantTo, idTo.Value, entryTo ); + var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slotFrom ), variantFrom ); + var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ); + var manipulationFrom = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, entryFrom ); + var manipulationTo = new ImcManipulation( slotTo, variantTo, idTo.Value, entryTo ); var imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId ); @@ -292,18 +364,23 @@ public static class EquipmentSwap public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, ref bool dataWasChanged ) + => CreateMtrl( redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged ); + + public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged ) { - var prefix = slot.IsAccessory() ? 'a' : 'e'; + var prefix = slotTo.IsAccessory() ? 'a' : 'e'; if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) { return null; } - var folderTo = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); + var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); var pathTo = $"{folderTo}{fileName}"; - var folderFrom = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); + var folderFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); var newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom ); + newFileName = ItemSwap.ReplaceSlot( newFileName, slotTo, slotFrom, slotTo != slotFrom ); var pathFrom = $"{folderFrom}{newFileName}"; if( newFileName != fileName ) @@ -318,7 +395,7 @@ public static class EquipmentSwap foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) { - var tex = CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); + var tex = CreateTex( redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( tex ); } @@ -326,6 +403,9 @@ public static class EquipmentSwap } public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) + => CreateTex( redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged ); + + public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) { var path = texture.Path; var addedDashes = false; @@ -340,6 +420,7 @@ public static class EquipmentSwap } var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); + newPath = ItemSwap.ReplaceSlot( newPath, slotTo, slotFrom, slotTo != slotFrom ); newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" ); if( newPath != path ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 2369a92d..e479afc1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -17,12 +17,6 @@ public static class ItemSwap public class InvalidItemTypeException : Exception { } - public class InvalidImcException : Exception - { } - - public class IdUnavailableException : Exception - { } - public class MissingFileException : Exception { public readonly ResourceType Type; @@ -224,6 +218,11 @@ public static class ItemSwap ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) : path; + public static string ReplaceSlot( string path, EquipSlot from, EquipSlot to, bool condition = true ) + => condition + ? path.Replace( $"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_" ) + : path; + public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 66d4851e..6b6e3111 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Penumbra.Mods.ItemSwap; @@ -133,6 +133,15 @@ public class ItemSwapContainer return ret; } + public Item[] LoadTypeSwap( EquipSlot slotFrom, Item from, EquipSlot slotTo, Item to, ModCollection? collection = null ) + { + Swaps.Clear(); + Loaded = false; + var ret = EquipmentSwap.CreateTypeSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + Loaded = true; + return ret; + } + public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) { var pathResolver = PathResolver( collection ); diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index cd551088..6c7ae4c6 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -34,6 +34,7 @@ public class ItemSwapWindow : IDisposable Necklace, Bracelet, Ring, + BetweenSlots, Hair, Face, Ears, @@ -101,6 +102,8 @@ public class ItemSwapWindow : IDisposable private int _targetId = 0; private int _sourceId = 0; private Exception? _loadException = null; + private EquipSlot _slotFrom = EquipSlot.Head; + private EquipSlot _slotTo = EquipSlot.Ears; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; @@ -164,6 +167,15 @@ public class ItemSwapWindow : IDisposable _useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing ); } + break; + case SwapType.BetweenSlots: + var (_, _, selectorFrom) = GetAccessorySelector( _slotFrom, true ); + var (_, _, selectorTo) = GetAccessorySelector( _slotTo, false ); + if( selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null ) + { + _affectedItems = _swapData.LoadTypeSwap( _slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, @@ -364,6 +376,7 @@ public class ItemSwapWindow : IDisposable DrawEquipmentSwap( SwapType.Necklace ); DrawEquipmentSwap( SwapType.Bracelet ); DrawEquipmentSwap( SwapType.Ring ); + DrawAccessorySwap(); DrawHairSwap(); DrawFaceSwap(); DrawEarSwap(); @@ -373,7 +386,7 @@ public class ItemSwapWindow : IDisposable private ImRaii.IEndObject DrawTab( SwapType newTab ) { - using var tab = ImRaii.TabItem( newTab.ToString() ); + using var tab = ImRaii.TabItem( newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString() ); if( tab ) { _dirty |= _lastTab != newTab; @@ -385,6 +398,99 @@ public class ItemSwapWindow : IDisposable return tab; } + private void DrawAccessorySwap() + { + using var tab = DrawTab( SwapType.BetweenSlots ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 3, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableSetupColumn( "##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "and put them on these" ).X ); + + var (article1, article2, selector) = GetAccessorySelector( _slotFrom, true ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( $"Take {article1}" ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) ) + { + if( combo ) + { + foreach( var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head) ) + { + if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom ) + { + _dirty = true; + _slotFrom = slot; + if( slot == _slotTo ) + { + _slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s ); + } + } + } + } + } + + ImGui.TableNextColumn(); + _dirty |= selector.Draw( "##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + (article1, _, selector) = GetAccessorySelector( _slotTo, false ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( $"and put {article2} on {article1}" ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) ) + { + if( combo ) + { + foreach( var slot in EquipSlotExtensions.AccessorySlots.Where( s => s != _slotFrom ) ) + { + if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo ) + { + _dirty = true; + _slotTo = slot; + } + } + } + } + + ImGui.TableNextColumn(); + + _dirty |= selector.Draw( "##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + if( _affectedItems is { Length: > 1 } ) + { + ImGui.SameLine(); + ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, selector.CurrentSelection.Item2 ) ) + .Select( i => i.Name.ToDalamudString().TextValue ) ) ); + } + } + } + + private (string, string, ItemSelector) GetAccessorySelector( EquipSlot slot, bool source ) + { + var (type, article1, article2) = slot switch + { + EquipSlot.Head => (SwapType.Hat, "this", "it"), + EquipSlot.Ears => (SwapType.Earrings, "these", "them"), + EquipSlot.Neck => (SwapType.Necklace, "this", "it"), + EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"), + EquipSlot.RFinger => (SwapType.Ring, "this", "it"), + EquipSlot.LFinger => (SwapType.Ring, "this", "it"), + _ => (SwapType.Ring, "this", "it"), + }; + var tuple = _selectors[ type ]; + return (article1, article2, source ? tuple.Source : tuple.Target); + } + private void DrawEquipmentSwap( SwapType type ) { using var tab = DrawTab( type );