diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs
index 89f2f58e..3d70c13d 100644
--- a/Penumbra.GameData/Data/GamePaths.cs
+++ b/Penumbra.GameData/Data/GamePaths.cs
@@ -61,6 +61,24 @@ public static partial class GamePaths
public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0')
=> $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
}
+
+ public static partial class Sklb
+ {
+ public static string Path(SetId monsterId)
+ => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.sklb";
+ }
+
+ public static partial class Skp
+ {
+ public static string Path(SetId monsterId)
+ => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.skp";
+ }
+
+ public static partial class Eid
+ {
+ public static string Path(SetId monsterId)
+ => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/eid_m{monsterId.Value:D4}b0001.eid";
+ }
}
public static partial class Weapon
@@ -243,6 +261,21 @@ public static partial class GamePaths
}
}
+ public static partial class Skeleton
+ {
+ public static partial class Phyb
+ {
+ public static string Path(GenderRace raceCode, string slot, SetId slotId)
+ => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.phyb";
+ }
+
+ public static partial class Sklb
+ {
+ public static string Path(GenderRace raceCode, string slot, SetId slotId)
+ => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.sklb";
+ }
+ }
+
public static partial class Character
{
public static partial class Mdl
@@ -254,18 +287,6 @@ public static partial class GamePaths
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl";
}
- public static partial class Phyb
- {
- public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId)
- => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.phyb";
- }
-
- public static partial class Sklb
- {
- public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId)
- => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.sklb";
- }
-
public static partial class Mtrl
{
// [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")]
diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs
index b21148ef..3891cc6f 100644
--- a/Penumbra/Meta/Manipulations/EstManipulation.cs
+++ b/Penumbra/Meta/Manipulations/EstManipulation.cs
@@ -19,6 +19,16 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation >
Head = CharacterUtility.Index.HeadEst,
}
+ public static string ToName( EstType type )
+ => type switch
+ {
+ EstType.Hair => "hair",
+ EstType.Face => "face",
+ EstType.Body => "top",
+ EstType.Head => "met",
+ _ => "unk",
+ };
+
public ushort Entry { get; private init; } // SkeletonIdx.
[JsonConverter( typeof( StringEnumConverter ) )]
diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs
index e442ae8e..b9e9a94f 100644
--- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs
+++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs
@@ -7,8 +7,6 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
-using Penumbra.Meta.Files;
-using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods.ItemSwap;
@@ -54,33 +52,6 @@ public static class CustomizationSwap
return true;
}
- public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true )
- => condition
- ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" )
- : path;
-
- public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true )
- => ReplaceAnyId( path, 'c', ( ushort )to, condition );
-
- public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true )
- => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition );
-
- public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true )
- => condition
- ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" )
- : path;
-
- public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true )
- => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition );
-
- public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true )
- => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition );
-
- public static string AddSuffix( string path, string ext, string suffix, bool condition = true )
- => condition
- ? path.Replace( ext, suffix + ext )
- : path;
-
public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant,
ref string fileName, ref bool dataWasChanged, out FileSwap mtrl )
{
@@ -89,10 +60,10 @@ public static class CustomizationSwap
var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant );
var newFileName = fileName;
- newFileName = ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race );
- newFileName = ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value );
- newFileName = AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race );
- newFileName = AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value );
+ newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race );
+ newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value );
+ newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race );
+ newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value );
var actualMtrlFromPath = mtrlFromPath;
if( newFileName != fileName )
@@ -142,9 +113,9 @@ public static class CustomizationSwap
}
}
- var newPath = ReplaceAnyRace( path, race );
- newPath = ReplaceAnyBody( newPath, slot, idFrom );
- newPath = AddSuffix( newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}", true );
+ var newPath = ItemSwap.ReplaceAnyRace( path, race );
+ newPath = ItemSwap.ReplaceAnyBody( newPath, slot, idFrom );
+ newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true );
if( newPath != path )
{
texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath;
@@ -160,56 +131,4 @@ public static class CustomizationSwap
var path = $"shader/sm5/shpk/{shaderName}";
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk );
}
-
- /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue.
- public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > metaChanges, BodySlot slot, GenderRace gr, SetId idFrom,
- SetId idTo, out MetaSwap? est )
- {
- var (gender, race) = gr.Split();
- var estSlot = slot switch
- {
- BodySlot.Hair => EstManipulation.EstType.Hair,
- BodySlot.Body => EstManipulation.EstType.Body,
- _ => ( EstManipulation.EstType )0,
- };
- if( estSlot == 0 )
- {
- est = null;
- return true;
- }
-
- var fromDefault = new EstManipulation( gender, race, estSlot, idFrom.Value, EstFile.GetDefault( estSlot, gr, idFrom.Value ) );
- var toDefault = new EstManipulation( gender, race, estSlot, idTo.Value, EstFile.GetDefault( estSlot, gr, idTo.Value ) );
- est = new MetaSwap( metaChanges, fromDefault, toDefault );
-
- if( est.SwapApplied.Est.Entry >= 2 )
- {
- if( !CreatePhyb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var phyb ) )
- {
- return false;
- }
-
- if( !CreateSklb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var sklb ) )
- {
- return false;
- }
-
- est.ChildSwaps.Add( phyb );
- est.ChildSwaps.Add( sklb );
- }
-
- return true;
- }
-
- public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap phyb )
- {
- var phybPath = GamePaths.Character.Phyb.Path( race, slot, estEntry );
- return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb );
- }
-
- public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap sklb )
- {
- var sklbPath = GamePaths.Character.Sklb.Path( race, slot, estEntry );
- return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb );
- }
}
\ No newline at end of file
diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs
new file mode 100644
index 00000000..f065d6be
--- /dev/null
+++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs
@@ -0,0 +1,406 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Lumina.Data.Parsing;
+using Lumina.Excel.GeneratedSheets;
+using Penumbra.GameData.Data;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Files;
+using Penumbra.GameData.Structs;
+using Penumbra.Interop.Structs;
+using Penumbra.Meta.Files;
+using Penumbra.Meta.Manipulations;
+using Penumbra.String.Classes;
+
+namespace Penumbra.Mods.ItemSwap;
+
+public static class EquipmentSwap
+{
+ public static Item[] CreateItemSwap( List< Swap > swaps, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, Item itemFrom,
+ Item itemTo )
+ {
+ // Check actual ids, variants and slots. We only support using the same slot.
+ LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom );
+ LookupItem( itemTo, out var slotTo, out var idTo, out var variantTo );
+ if( slotFrom != slotTo )
+ {
+ throw new ItemSwap.InvalidItemTypeException();
+ }
+
+ if( !CreateEqp( manips, slotFrom, idFrom, idTo, out var eqp ) )
+ {
+ throw new Exception( "Could not get Eqp Entry for Swap." );
+ }
+
+ if( eqp != null )
+ {
+ swaps.Add( eqp );
+ }
+
+ if( !CreateGmp( manips, slotFrom, idFrom, idTo, out var gmp ) )
+ {
+ throw new Exception( "Could not get Gmp Entry for Swap." );
+ }
+
+ if( gmp != null )
+ {
+ swaps.Add( gmp );
+ }
+
+
+ var (imcFileFrom, variants, affectedItems) = GetVariants( slotFrom, idFrom, idTo, variantFrom );
+ var imcFileTo = new ImcFile( new ImcManipulation( slotFrom, variantTo, idTo.Value, default ) );
+
+ var isAccessory = slotFrom.IsAccessory();
+ var estType = slotFrom switch
+ {
+ EquipSlot.Head => EstManipulation.EstType.Head,
+ EquipSlot.Body => EstManipulation.EstType.Body,
+ _ => ( EstManipulation.EstType )0,
+ };
+
+ var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotFrom ), variantTo ).MaterialId;
+ foreach( var gr in Enum.GetValues< GenderRace >() )
+ {
+ if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 )
+ {
+ continue;
+ }
+
+ if( !ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, out var est ) )
+ {
+ throw new Exception( "Could not get Est Entry for Swap." );
+ }
+
+ if( est != null )
+ {
+ swaps.Add( est );
+ }
+
+ if( !CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo, out var eqdp ) )
+ {
+ throw new Exception( "Could not get Eqdp Entry for Swap." );
+ }
+
+ if( eqdp != null )
+ {
+ swaps.Add( eqdp );
+ }
+ }
+
+ foreach( var variant in variants )
+ {
+ if( !CreateImc( redirections, manips, slotFrom, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo, out var imc ) )
+ {
+ throw new Exception( "Could not get IMC Entry for Swap." );
+ }
+
+ swaps.Add( imc );
+ }
+
+
+ return affectedItems;
+ }
+
+ public static bool CreateEqdp( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom,
+ SetId idTo, byte mtrlTo, out MetaSwap? meta )
+ {
+ 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 );
+ meta = new MetaSwap( manips, eqdpFrom, eqdpTo );
+ var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slot );
+ if( ownMdl )
+ {
+ if( !CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo, out var mdl ) )
+ {
+ return false;
+ }
+
+ meta.ChildSwaps.Add( mdl );
+ }
+ else if( !ownMtrl && meta.SwapAppliedIsDefault )
+ {
+ meta = null;
+ }
+
+ return true;
+ }
+
+ public static bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo,
+ out FileSwap mdl )
+ {
+ var mdlPathFrom = GamePaths.Equipment.Mdl.Path( idFrom, gr, slot );
+ var mdlPathTo = GamePaths.Equipment.Mdl.Path( idTo, gr, slot );
+ if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) )
+ {
+ return false;
+ }
+
+ foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() )
+ {
+ if( !CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged, out var mtrl ) )
+ {
+ return false;
+ }
+
+ if( mtrl != null )
+ {
+ mdl.ChildSwaps.Add( mtrl );
+ }
+ }
+
+ return true;
+ }
+
+ private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot )
+ {
+ var model = GenderRace.Unknown;
+ var material = GenderRace.Unknown;
+ var accessory = slot.IsAccessory();
+ foreach( var gr in genderRace.Dependencies() )
+ {
+ var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value );
+ var (b1, b2) = entry.ToBits( slot );
+ if( b1 && material == GenderRace.Unknown )
+ {
+ material = gr;
+ if( model != GenderRace.Unknown )
+ {
+ return ( model, material );
+ }
+ }
+
+ if( b2 && model == GenderRace.Unknown )
+ {
+ model = gr;
+ if( material != GenderRace.Unknown )
+ {
+ return ( model, material );
+ }
+ }
+ }
+
+ return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale );
+ }
+
+ private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant )
+ {
+ slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
+ if( !slot.IsEquipmentPiece() )
+ {
+ throw new ItemSwap.InvalidItemTypeException();
+ }
+
+ modelId = ( ( Quad )i.ModelMain ).A;
+ variant = ( byte )( ( Quad )i.ModelMain ).B;
+ }
+
+ private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom )
+ {
+ var entry = new ImcManipulation( slot, 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();
+ 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();
+ variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray();
+ }
+
+ return ( imc, variants, items );
+ }
+
+ public static bool CreateGmp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? gmp )
+ {
+ if( slot is not EquipSlot.Head )
+ {
+ gmp = null;
+ return true;
+ }
+
+ var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value );
+ var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value );
+ gmp = new MetaSwap( manips, manipFrom, manipTo );
+ return true;
+ }
+
+ public static bool CreateImc( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo,
+ byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo, out MetaSwap imc )
+ {
+ 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 );
+ imc = new MetaSwap( manips, manipulationFrom, manipulationTo );
+
+ if( !AddDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId, imc ) )
+ {
+ return false;
+ }
+
+ if( !AddAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId, imc ) )
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc )
+ {
+ if( decalId != 0 )
+ {
+ var decalPath = GamePaths.Equipment.Decal.Path( decalId );
+ if( !FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath, out var swap ) )
+ {
+ return false;
+ }
+
+ imc.ChildSwaps.Add( swap );
+ }
+
+ return true;
+ }
+
+ public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc )
+ {
+ if( vfxId != 0 )
+ {
+ var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId );
+ var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId );
+ if( !FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo, out var swap ) )
+ {
+ return false;
+ }
+
+ foreach( ref var filePath in swap.AsAvfx()!.Textures.AsSpan() )
+ {
+ if( !CreateAtex( redirections, ref filePath, ref swap.DataWasChanged, out var atex ) )
+ {
+ return false;
+ }
+
+ swap.ChildSwaps.Add( atex );
+ }
+
+ imc.ChildSwaps.Add( swap );
+ }
+
+ return true;
+ }
+
+ public static bool CreateEqp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? eqp )
+ {
+ if( slot.IsAccessory() )
+ {
+ eqp = null;
+ return true;
+ }
+
+ var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value );
+ var eqpValueTo = ExpandedEqpFile.GetDefault( idTo.Value );
+ var eqpFrom = new EqpManipulation( eqpValueFrom, slot, idFrom.Value );
+ var eqpTo = new EqpManipulation( eqpValueTo, slot, idFrom.Value );
+ eqp = new MetaSwap( manips, eqpFrom, eqpTo );
+ return true;
+ }
+
+ public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName,
+ ref bool dataWasChanged, out FileSwap? mtrl )
+ {
+ var prefix = slot.IsAccessory() ? 'a' : 'e';
+ if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) )
+ {
+ mtrl = null;
+ return true;
+ }
+
+ var folderTo = slot.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 newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom );
+ var pathFrom = $"{folderFrom}{newFileName}";
+
+ if( newFileName != fileName )
+ {
+ fileName = newFileName;
+ dataWasChanged = true;
+ }
+
+ if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo, out mtrl ) )
+ {
+ return false;
+ }
+
+ if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shader ) )
+ {
+ return false;
+ }
+
+ mtrl.ChildSwaps.Add( shader );
+
+ foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() )
+ {
+ if( !CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged, out var swap ) )
+ {
+ return false;
+ }
+
+ mtrl.ChildSwaps.Add( swap );
+ }
+
+ return true;
+ }
+
+ public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture,
+ ref bool dataWasChanged,
+ out FileSwap tex )
+ {
+ var path = texture.Path;
+ var addedDashes = false;
+ if( texture.DX11 )
+ {
+ var fileName = Path.GetFileName( path );
+ if( !fileName.StartsWith( "--" ) )
+ {
+ path = path.Replace( fileName, $"--{fileName}" );
+ addedDashes = true;
+ }
+ }
+
+ var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom );
+ newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true );
+ if( newPath != path )
+ {
+ texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath;
+ dataWasChanged = true;
+ }
+
+ return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path );
+ }
+
+ public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk )
+ {
+ var path = $"shader/sm5/shpk/{shaderName}";
+ return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk );
+ }
+
+ public static bool CreateAtex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged, out FileSwap atex )
+ {
+ var oldPath = filePath;
+ filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}", true );
+ dataWasChanged = true;
+
+ return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, out atex, oldPath );
+ }
+}
\ No newline at end of file
diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs
index 3771cd6d..d32e2073 100644
--- a/Penumbra/Mods/ItemSwap/ItemSwap.cs
+++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs
@@ -1,7 +1,14 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Text.RegularExpressions;
+using Penumbra.GameData.Data;
+using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
+using Penumbra.GameData.Structs;
+using Penumbra.Meta.Files;
+using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods.ItemSwap;
@@ -110,6 +117,76 @@ public static class ItemSwap
return false;
}
+ public static bool LoadAvfx( FullPath path, [NotNullWhen( true )] out AvfxFile? file )
+ {
+ try
+ {
+ if( LoadFile( path, out byte[] data ) )
+ {
+ file = new AvfxFile( data );
+ return true;
+ }
+ }
+ catch( Exception e )
+ {
+ Penumbra.Log.Debug( $"Could not parse file {path} to Avfx:\n{e}" );
+ }
+
+ file = null;
+ return false;
+ }
+
+
+ public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap phyb )
+ {
+ var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry );
+ return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb );
+ }
+
+ public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap sklb )
+ {
+ var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry );
+ return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb );
+ }
+
+ /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue.
+ public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EstManipulation.EstType type,
+ GenderRace genderRace, SetId idFrom, SetId idTo, out MetaSwap? est )
+ {
+ if( type == 0 )
+ {
+ est = null;
+ return true;
+ }
+
+ var (gender, race) = genderRace.Split();
+ var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) );
+ var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) );
+ est = new MetaSwap( manips, fromDefault, toDefault );
+
+ if( est.SwapApplied.Est.Entry >= 2 )
+ {
+ if( !CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var phyb ) )
+ {
+ return false;
+ }
+
+ if( !CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var sklb ) )
+ {
+ return false;
+ }
+
+ est.ChildSwaps.Add( phyb );
+ est.ChildSwaps.Add( sklb );
+ }
+ else if( est.SwapAppliedIsDefault )
+ {
+ est = null;
+ }
+
+ return true;
+ }
+
public static int GetStableHashCode( this string str )
{
unchecked
@@ -131,4 +208,31 @@ public static class ItemSwap
return hash1 + hash2 * 1566083941;
}
}
+
+ public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true )
+ => condition
+ ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" )
+ : path;
+
+ public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true )
+ => ReplaceAnyId( path, 'c', ( ushort )to, condition );
+
+ public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true )
+ => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition );
+
+ public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true )
+ => condition
+ ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" )
+ : path;
+
+ public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true )
+ => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition );
+
+ public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true )
+ => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition );
+
+ public static string AddSuffix( string path, string ext, string suffix, bool condition = true )
+ => condition
+ ? path.Replace( ext, suffix + ext )
+ : path;
}
\ No newline at end of file
diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs
index 4fc07cd4..8027e7d1 100644
--- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs
+++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
@@ -21,6 +22,7 @@ public class ItemSwapContainer
=> _modManipulations;
public readonly List< Swap > Swaps = new();
+
public bool Loaded { get; private set; }
public void Clear()
@@ -109,6 +111,22 @@ public class ItemSwapContainer
LoadMod( null, null );
}
+ public Item[] LoadEquipment( Item from, Item to )
+ {
+ try
+ {
+ Swaps.Clear();
+ var ret = EquipmentSwap.CreateItemSwap( Swaps, ModRedirections, _modManipulations, from, to );
+ Loaded = true;
+ return ret;
+ }
+ catch( Exception e )
+ {
+ Swaps.Clear();
+ Loaded = false;
+ return Array.Empty< Item >();
+ }
+ }
public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to )
{
@@ -117,7 +135,13 @@ public class ItemSwapContainer
return false;
}
- if( !CustomizationSwap.CreateEst( ModRedirections, _modManipulations, slot, race, from, to, out var est ) )
+ var type = slot switch
+ {
+ BodySlot.Hair => EstManipulation.EstType.Hair,
+ BodySlot.Face => EstManipulation.EstType.Face,
+ _ => ( EstManipulation.EstType )0,
+ };
+ if( !ItemSwap.CreateEst( ModRedirections, _modManipulations, type, race, from, to, out var est ) )
{
return false;
}
diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs
index d425e476..5018f2a7 100644
--- a/Penumbra/Mods/ItemSwap/Swaps.cs
+++ b/Penumbra/Mods/ItemSwap/Swaps.cs
@@ -1,4 +1,3 @@
-using System;
using Penumbra.GameData.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
@@ -13,7 +12,7 @@ namespace Penumbra.Mods.ItemSwap;
public class Swap
{
/// Any further swaps belonging specifically to this tree of changes.
- public List< Swap > ChildSwaps = new();
+ public readonly List< Swap > ChildSwaps = new();
public IEnumerable< Swap > WithChildren()
=> ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this );
@@ -111,6 +110,9 @@ public sealed class FileSwap : Swap
public MtrlFile? AsMtrl()
=> FileData as MtrlFile;
+ public AvfxFile? AsAvfx()
+ => FileData as AvfxFile;
+
///
/// Create a full swap container for a specific file type using a modded redirection set, the actually requested path and the game file it should load instead after the swap.
///
@@ -151,6 +153,7 @@ public sealed class FileSwap : Swap
{
ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
+ ResourceType.Avfx => ItemSwap.LoadAvfx( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
_ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
};
diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs
index a1329a6d..2d23abc4 100644
--- a/Penumbra/UI/Classes/ItemSwapWindow.cs
+++ b/Penumbra/UI/Classes/ItemSwapWindow.cs
@@ -5,6 +5,7 @@ using Dalamud.Interface;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
+using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
@@ -18,17 +19,43 @@ namespace Penumbra.UI.Classes;
public class ItemSwapWindow : IDisposable
{
- private class ItemSelector : FilterComboCache< Item >
+ private class EquipSelector : FilterComboCache< Item >
{
- public ItemSelector()
+ public EquipSelector()
: base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i
- => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipmentPiece() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) )
+ => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipment() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) )
{ }
protected override string ToString( Item obj )
=> obj.Name.ToString();
}
+ private class AccessorySelector : FilterComboCache< Item >
+ {
+ public AccessorySelector()
+ : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i
+ => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsAccessory() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) )
+ { }
+
+ protected override string ToString( Item obj )
+ => obj.Name.ToString();
+ }
+
+ private class SlotSelector : FilterComboCache< Item >
+ {
+ public readonly EquipSlot CurrentSlot;
+
+ public SlotSelector( EquipSlot slot )
+ : base( () => Dalamud.GameData.GetExcelSheet< Item >()!.Where( i
+ => ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot() == slot && i.ModelMain != 0 && i.Name.RawData.Length > 0 ).ToList() )
+ {
+ CurrentSlot = slot;
+ }
+
+ protected override string ToString( Item obj )
+ => obj.Name.ToString();
+ }
+
public ItemSwapWindow()
{
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
@@ -41,8 +68,10 @@ public class ItemSwapWindow : IDisposable
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
}
- private readonly ItemSelector _itemSelector = new();
- private readonly ItemSwapContainer _swapData = new();
+ private readonly EquipSelector _equipSelector = new();
+ private readonly AccessorySelector _accessorySelector = new();
+ private SlotSelector? _slotSelector;
+ private readonly ItemSwapContainer _swapData = new();
private Mod? _mod;
private ModSettings? _modSettings;
@@ -53,13 +82,14 @@ public class ItemSwapWindow : IDisposable
private ModelRace _currentRace = ModelRace.Midlander;
private int _targetId = 0;
private int _sourceId = 0;
- private int _currentVariant = 1;
private Exception? _loadException = null;
private string _newModName = string.Empty;
private string _newGroupName = "Swaps";
private string _newOptionName = string.Empty;
- private bool _useFileSwaps = false;
+ private bool _useFileSwaps = true;
+
+ private Item[]? _affectedItems;
public void UpdateMod( Mod mod, ModSettings? settings )
@@ -90,38 +120,38 @@ public class ItemSwapWindow : IDisposable
_swapData.Clear();
_loadException = null;
- if( _targetId > 0 && _sourceId > 0 )
+ try
{
- try
+ switch( _lastTab )
{
- switch( _lastTab )
- {
- case SwapType.Equipment: break;
- case SwapType.Accessory: break;
- case SwapType.Hair:
-
- _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
- break;
- case SwapType.Face:
- _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
- break;
- case SwapType.Ears:
- _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId );
- break;
- case SwapType.Tail:
- _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
- break;
- case SwapType.Weapon: break;
- case SwapType.Minion: break;
- case SwapType.Mount: break;
- }
- }
- catch( Exception e )
- {
- Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" );
- _loadException = e;
+ case SwapType.Equipment when _slotSelector?.CurrentSelection != null && _equipSelector.CurrentSelection != null:
+ _affectedItems = _swapData.LoadEquipment( _equipSelector.CurrentSelection, _slotSelector.CurrentSelection );
+ break;
+ case SwapType.Accessory when _slotSelector?.CurrentSelection != null && _accessorySelector.CurrentSelection != null:
+ _affectedItems = _swapData.LoadEquipment( _accessorySelector.CurrentSelection, _slotSelector.CurrentSelection );
+ break;
+ case SwapType.Hair when _targetId > 0 && _sourceId > 0:
+ _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
+ break;
+ case SwapType.Face when _targetId > 0 && _sourceId > 0:
+ _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
+ break;
+ case SwapType.Ears when _targetId > 0 && _sourceId > 0:
+ _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId );
+ break;
+ case SwapType.Tail when _targetId > 0 && _sourceId > 0:
+ _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
+ break;
+ case SwapType.Weapon: break;
+ case SwapType.Minion: break;
+ case SwapType.Mount: break;
}
}
+ catch( Exception e )
+ {
+ Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" );
+ _loadException = e;
+ }
_dirty = false;
}
@@ -174,7 +204,7 @@ public class ItemSwapWindow : IDisposable
ImGui.SameLine();
tt = "Create a new option inside this mod containing only the swap.";
if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt,
- true || (!newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) )) )
+ true || !newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) ) )
{ }
ImGui.SameLine();
@@ -201,15 +231,13 @@ public class ItemSwapWindow : IDisposable
{
using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None );
+ DrawArmorSwap();
+ DrawAccessorySwap();
DrawHairSwap();
DrawFaceSwap();
DrawEarSwap();
DrawTailSwap();
- DrawArmorSwap();
- DrawAccessorySwap();
DrawWeaponSwap();
- DrawMinionSwap();
- DrawMountSwap();
}
private ImRaii.IEndObject DrawTab( SwapType newTab )
@@ -218,7 +246,7 @@ public class ItemSwapWindow : IDisposable
if( tab )
{
_dirty |= _lastTab != newTab;
- _lastTab = newTab;
+ _lastTab = newTab;
}
UpdateState();
@@ -228,22 +256,60 @@ public class ItemSwapWindow : IDisposable
private void DrawArmorSwap()
{
- using var disabled = ImRaii.Disabled();
- using var tab = DrawTab( SwapType.Equipment );
+ using var tab = DrawTab( SwapType.Equipment );
if( !tab )
{
return;
}
+
+ using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted( "Take this piece of equipment" );
+ ImGui.TableNextColumn();
+ if( _equipSelector.Draw( "##itemTarget", _equipSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) )
+ {
+ var slot = ( ( EquipSlot )( _equipSelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot();
+ if( slot != _slotSelector?.CurrentSlot )
+ _slotSelector = new SlotSelector( slot );
+ _dirty = true;
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted( "And put it on this one" );
+ ImGui.TableNextColumn();
+ _slotSelector ??= new SlotSelector( EquipSlot.Unknown );
+ _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
}
private void DrawAccessorySwap()
{
- using var disabled = ImRaii.Disabled();
using var tab = DrawTab( SwapType.Accessory );
if( !tab )
{
return;
}
+
+ using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted( "Take this accessory" );
+ ImGui.TableNextColumn();
+ if( _accessorySelector.Draw( "##itemTarget", _accessorySelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) )
+ {
+ var slot = ( ( EquipSlot )( _accessorySelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot();
+ if( slot != _slotSelector?.CurrentSlot )
+ _slotSelector = new SlotSelector( slot );
+ _dirty = true;
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted( "And put it on this one" );
+ ImGui.TableNextColumn();
+ _slotSelector ??= new SlotSelector( EquipSlot.Unknown );
+ _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
}
private void DrawHairSwap()
@@ -286,7 +352,7 @@ public class ItemSwapWindow : IDisposable
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
DrawTargetIdInput( "Take this Tail Type" );
DrawSourceIdInput();
- DrawGenderInput("for all", 2);
+ DrawGenderInput( "for all", 2 );
}
@@ -315,26 +381,6 @@ public class ItemSwapWindow : IDisposable
}
}
- private void DrawMinionSwap()
- {
- using var disabled = ImRaii.Disabled();
- using var tab = DrawTab( SwapType.Minion );
- if( !tab )
- {
- return;
- }
- }
-
- private void DrawMountSwap()
- {
- using var disabled = ImRaii.Disabled();
- using var tab = DrawTab( SwapType.Mount );
- if( !tab )
- {
- return;
- }
- }
-
private const float InputWidth = 120;
private void DrawTargetIdInput( string text = "Take this ID" )
@@ -346,7 +392,10 @@ public class ItemSwapWindow : IDisposable
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) )
+ {
_targetId = Math.Clamp( _targetId, 0, byte.MaxValue );
+ }
+
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
}
@@ -358,21 +407,11 @@ public class ItemSwapWindow : IDisposable
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
- if (ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ))
+ if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) )
+ {
_sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue );
- _dirty |= ImGui.IsItemDeactivatedAfterEdit();
- }
+ }
- private void DrawVariantInput( string text )
- {
- ImGui.TableNextColumn();
- ImGui.AlignTextToFramePadding();
- ImGui.TextUnformatted( text );
-
- ImGui.TableNextColumn();
- ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
- if( ImGui.InputInt( "##variantId", ref _currentVariant, 0, 0 ) )
- _currentVariant = Math.Clamp( _currentVariant, 0, byte.MaxValue );
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
}
diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs
index 03f984c0..c3ab2a14 100644
--- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs
+++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs
@@ -184,7 +184,7 @@ public partial class ModEditWindow
{
_dialogManager.Draw();
- using var tab = ImRaii.TabItem( "Texture Import/Export (WIP)" );
+ using var tab = ImRaii.TabItem( "Texture Import/Export" );
if( !tab )
{
return;
diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs
index 886a47d2..f7a09c88 100644
--- a/Penumbra/UI/Classes/ModEditWindow.cs
+++ b/Penumbra/UI/Classes/ModEditWindow.cs
@@ -531,11 +531,11 @@ public partial class ModEditWindow : Window, IDisposable
public ModEditWindow()
: base( WindowBaseLabel )
{
- _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl",
+ _materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl",
() => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawMaterialPanel,
() => _mod?.ModPath.FullName ?? string.Empty );
- _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl",
+ _modelTab = new FileEditor< MdlFile >( "Models", ".mdl",
() => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawModelPanel,
() => _mod?.ModPath.FullName ?? string.Empty );