Meta stuff is terrible.

This commit is contained in:
Ottermandias 2023-04-16 13:18:43 +02:00
parent 0186f176d0
commit 1d82e882ed
35 changed files with 1265 additions and 1247 deletions

View file

@ -5,6 +5,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.String.Classes;
namespace Penumbra.Mods.ItemSwap;
@ -12,7 +13,7 @@ namespace Penumbra.Mods.ItemSwap;
public static class CustomizationSwap
{
/// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode.
public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo )
public static FileSwap CreateMdl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo )
{
if( idFrom.Value > byte.MaxValue )
{
@ -22,7 +23,7 @@ public static class CustomizationSwap
var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() );
var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() );
var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo );
var mdl = FileSwap.CreateSwap( manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo );
var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1;
foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() )
@ -31,7 +32,7 @@ public static class CustomizationSwap
foreach( var variant in Enumerable.Range( 1, range ) )
{
name = materialFileName;
var mtrl = CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged );
var mtrl = CreateMtrl( manager, redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged );
mdl.ChildSwaps.Add( mtrl );
}
@ -41,7 +42,7 @@ public static class CustomizationSwap
return mdl;
}
public static FileSwap CreateMtrl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant,
public static FileSwap CreateMtrl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant,
ref string fileName, ref bool dataWasChanged )
{
variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant;
@ -62,20 +63,20 @@ public static class CustomizationSwap
dataWasChanged = true;
}
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath );
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
var mtrl = FileSwap.CreateSwap( manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath );
var shpk = CreateShader( manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( shpk );
foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() )
{
var tex = CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged );
var tex = CreateTex( manager, redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( tex );
}
return mtrl;
}
public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture,
public static FileSwap CreateTex( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture,
ref bool dataWasChanged )
{
var path = texture.Path;
@ -99,13 +100,13 @@ public static class CustomizationSwap
dataWasChanged = true;
}
return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path );
return FileSwap.CreateSwap( manager, ResourceType.Tex, redirections, newPath, path, path );
}
public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged )
public static FileSwap CreateShader( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged )
{
var path = $"shader/sm5/shpk/{shaderName}";
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path );
return FileSwap.CreateSwap( manager, ResourceType.Shpk, redirections, path, path );
}
}

View file

@ -10,6 +10,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
@ -18,41 +19,51 @@ namespace Penumbra.Mods.ItemSwap;
public static class EquipmentSwap
{
private static EquipSlot[] ConvertSlots( EquipSlot slot, bool rFinger, bool lFinger )
private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger)
{
if( slot != EquipSlot.RFinger )
{
return new[] { slot };
}
if (slot != EquipSlot.RFinger)
return new[]
{
slot,
};
return rFinger
? lFinger
? new[] { EquipSlot.RFinger, EquipSlot.LFinger }
: new[] { EquipSlot.RFinger }
? new[]
{
EquipSlot.RFinger,
EquipSlot.LFinger,
}
: new[]
{
EquipSlot.RFinger,
}
: lFinger
? new[] { EquipSlot.LFinger }
: Array.Empty< EquipSlot >();
? new[]
{
EquipSlot.LFinger,
}
: Array.Empty<EquipSlot>();
}
public static Item[] CreateTypeSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips,
EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo )
public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, 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() )
{
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( identifier, slotFrom, idFrom, idTo, variantFrom );
var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default );
var imcFileTo = new ImcFile( imcManip );
var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation(slotTo, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile(manager, 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 >() )
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 )
switch (gr.Split().Item1)
{
case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue;
@ -60,22 +71,18 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue;
}
if( CharacterUtilityData.EqdpIdx( gr, true ) < 0 )
{
if (CharacterUtilityData.EqdpIdx(gr, true) < 0)
continue;
}
try
{
var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo );
if( eqdp != null )
{
swaps.Add( eqdp );
}
var eqdp = CreateEqdp(manager, redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo);
if (eqdp != null)
swaps.Add(eqdp);
}
catch( ItemSwap.MissingFileException e )
catch (ItemSwap.MissingFileException e)
{
switch( gr )
switch (gr)
{
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
skipMale = true;
@ -88,59 +95,54 @@ public static class EquipmentSwap
}
}
foreach( var variant in variants )
foreach (var variant in variants)
{
var imc = CreateImc( redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo );
swaps.Add( imc );
var imc = CreateImc(manager, redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add(imc);
}
return affectedItems;
}
public static Item[] CreateItemSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom,
Item itemTo, bool rFinger = true, bool lFinger = true )
public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, Item itemFrom,
Item itemTo, bool rFinger = true, bool lFinger = true)
{
// 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 )
{
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();
}
var eqp = CreateEqp( manips, slotFrom, idFrom, idTo );
if( eqp != null )
{
swaps.Add( eqp );
}
var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo);
if (eqp != null)
swaps.Add(eqp);
var gmp = CreateGmp( manips, slotFrom, idFrom, idTo );
if( gmp != null )
{
swaps.Add( gmp );
}
var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo);
if (gmp != null)
swaps.Add(gmp);
var affectedItems = Array.Empty< Item >();
foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) )
var affectedItems = Array.Empty<Item>();
foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger))
{
( var imcFileFrom, var variants, affectedItems ) = GetVariants( identifier, slot, idFrom, idTo, variantFrom );
var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default );
var imcFileTo = new ImcFile( imcManip );
(var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation(slot, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile(manager, imcManip);
var isAccessory = slot.IsAccessory();
var estType = slot switch
{
EquipSlot.Head => EstManipulation.EstType.Head,
EquipSlot.Body => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0,
_ => (EstManipulation.EstType)0,
};
var skipFemale = false;
var skipMale = false;
var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ) ) ).Imc.Entry.MaterialId;
foreach( var gr in Enum.GetValues< GenderRace >() )
var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId;
foreach (var gr in Enum.GetValues<GenderRace>())
{
switch( gr.Split().Item1 )
switch (gr.Split().Item1)
{
case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue;
@ -148,30 +150,24 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue;
}
if( CharacterUtilityData.EqdpIdx( gr, isAccessory ) < 0 )
{
if (CharacterUtilityData.EqdpIdx(gr, isAccessory) < 0)
continue;
}
try
{
var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo );
if( eqdp != null )
{
swaps.Add( eqdp );
}
var eqdp = CreateEqdp(manager, redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo);
if (eqdp != null)
swaps.Add(eqdp);
var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false;
var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl );
if( est != null )
{
swaps.Add( est );
}
var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false;
var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl);
if (est != null)
swaps.Add(est);
}
catch( ItemSwap.MissingFileException e )
catch (ItemSwap.MissingFileException e)
{
switch( gr )
switch (gr)
{
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
skipMale = true;
@ -184,33 +180,38 @@ public static class EquipmentSwap
}
}
foreach( var variant in variants )
foreach (var variant in variants)
{
var imc = CreateImc( redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo );
swaps.Add( imc );
var imc = CreateImc(manager, redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add(imc);
}
}
return affectedItems;
}
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 )
public static MetaSwap? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, GenderRace gr, SetId idFrom,
SetId idTo, byte mtrlTo)
=> CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo);
public static MetaSwap? CreateEqdp(MetaFileManager manager, 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, 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( slotFrom );
if( ownMdl )
var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom.Value), slotFrom, gender,
race, idFrom.Value);
var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, 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(slotFrom);
if (ownMdl)
{
var mdl = CreateMdl( redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo );
meta.ChildSwaps.Add( mdl );
var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo);
meta.ChildSwaps.Add(mdl);
}
else if( !ownMtrl && meta.SwapAppliedIsDefault )
else if (!ownMtrl && meta.SwapAppliedIsDefault)
{
meta = null;
}
@ -218,97 +219,98 @@ public static class EquipmentSwap
return meta;
}
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(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, GenderRace gr,
SetId idFrom, SetId idTo, byte mtrlTo)
=> CreateMdl(manager, 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 )
public static FileSwap CreateMdl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo)
{
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 );
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(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo);
foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() )
foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan())
{
var mtrl = CreateMtrl( redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged );
if( mtrl != null )
{
mdl.ChildSwaps.Add( mtrl );
}
var mtrl = CreateMtrl(manager, redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged);
if (mtrl != null)
mdl.ChildSwaps.Add(mtrl);
}
return mdl;
}
private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant )
private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant)
{
slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
if( !slot.IsEquipmentPiece() )
{
slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot();
if (!slot.IsEquipmentPiece())
throw new ItemSwap.InvalidItemTypeException();
}
modelId = ( ( Quad )i.ModelMain ).A;
variant = ( byte )( ( Quad )i.ModelMain ).B;
modelId = ((Quad)i.ModelMain).A;
variant = (byte)((Quad)i.ModelMain).B;
}
private static (ImcFile, byte[], Item[]) GetVariants( IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom )
private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom,
SetId idFrom, SetId idTo, byte variantFrom)
{
var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default );
var imc = new ImcFile( entry );
var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default);
var imc = new ImcFile(manager, entry);
Item[] items;
byte[] variants;
if( idFrom.Value == idTo.Value )
if (idFrom.Value == idTo.Value)
{
items = identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray();
variants = new[] { variantFrom };
items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray();
variants = new[]
{
variantFrom,
};
}
else
{
items = 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();
items = 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();
}
return ( imc, variants, items );
return (imc, variants, items);
}
public static MetaSwap? CreateGmp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo )
public static MetaSwap? CreateGmp(MetaFileManager manager, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, SetId idFrom,
SetId idTo)
{
if( slot is not EquipSlot.Head )
{
if (slot is not EquipSlot.Head)
return null;
}
var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value );
var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value );
return new MetaSwap( manips, manipFrom, manipTo );
var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom.Value), idFrom.Value);
var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo.Value), idTo.Value);
return new MetaSwap(manips, manipFrom, manipTo);
}
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(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot,
SetId idFrom, SetId idTo,
byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
=> CreateImc(manager, 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 )
public static MetaSwap CreateImc(MetaFileManager manager, 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( 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 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 );
if( decal != null )
{
imc.ChildSwaps.Add( decal );
}
var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId);
if (decal != null)
imc.ChildSwaps.Add(decal);
var avfx = CreateAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId );
if( avfx != null )
{
imc.ChildSwaps.Add( avfx );
}
var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId);
if (avfx != null)
imc.ChildSwaps.Add(avfx);
// IMC also controls sound, Example: Dodore Doublet, but unknown what it does?
// IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does.
@ -316,134 +318,135 @@ public static class EquipmentSwap
}
// Example: Crimson Standard Bracelet
public static FileSwap? CreateDecal( Func< Utf8GamePath, FullPath > redirections, byte decalId )
public static FileSwap? CreateDecal(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, byte decalId)
{
if( decalId == 0 )
{
if (decalId == 0)
return null;
}
var decalPath = GamePaths.Equipment.Decal.Path( decalId );
return FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath );
var decalPath = GamePaths.Equipment.Decal.Path(decalId);
return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath);
}
// Example: Abyssos Helm / Body
public static FileSwap? CreateAvfx( Func< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId )
public static FileSwap? CreateAvfx(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, SetId idFrom, SetId idTo, byte vfxId)
{
if( vfxId == 0 )
{
if (vfxId == 0)
return null;
}
var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId );
var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId );
var avfx = FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo );
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom.Value, vfxId);
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo.Value, vfxId);
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
foreach( ref var filePath in avfx.AsAvfx()!.Textures.AsSpan() )
foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan())
{
var atex = CreateAtex( redirections, ref filePath, ref avfx.DataWasChanged );
avfx.ChildSwaps.Add( atex );
var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged);
avfx.ChildSwaps.Add(atex);
}
return avfx;
}
public static MetaSwap? CreateEqp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo )
public static MetaSwap? CreateEqp(MetaFileManager manager, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, SetId idFrom,
SetId idTo)
{
if( slot.IsAccessory() )
{
if (slot.IsAccessory())
return null;
}
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 );
return new MetaSwap( manips, eqpFrom, eqpTo );
var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom.Value);
var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo.Value);
var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom.Value);
var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom.Value);
return new MetaSwap(manips, eqpFrom, eqpTo);
}
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(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, SetId idFrom,
SetId idTo, byte variantTo, ref string fileName,
ref bool dataWasChanged)
=> CreateMtrl(manager, 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 )
public static FileSwap? CreateMtrl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
SetId idFrom, SetId idTo, byte variantTo, ref string fileName,
ref bool dataWasChanged)
{
var prefix = slotTo.IsAccessory() ? 'a' : 'e';
if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) )
{
if (!fileName.Contains($"{prefix}{idTo.Value:D4}"))
return null;
}
var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo );
var pathTo = $"{folderTo}{fileName}";
var folderTo = slotTo.IsAccessory()
? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo)
: GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo);
var pathTo = $"{folderTo}{fileName}";
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}";
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 )
if (newFileName != fileName)
{
fileName = newFileName;
dataWasChanged = true;
}
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo );
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( shpk );
var mtrl = FileSwap.CreateSwap(manager, ResourceType.Mtrl, redirections, pathFrom, pathTo);
var shpk = CreateShader(manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged);
mtrl.ChildSwaps.Add(shpk);
foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() )
foreach (ref var texture in mtrl.AsMtrl()!.Textures.AsSpan())
{
var tex = CreateTex( redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( tex );
var tex = CreateTex(manager, redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged);
mtrl.ChildSwaps.Add(tex);
}
return mtrl;
}
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(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, SetId idFrom, SetId idTo,
ref MtrlFile.Texture texture, ref bool dataWasChanged)
=> CreateTex(manager, 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 )
public static FileSwap CreateTex(MetaFileManager manager, 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;
if( texture.DX11 )
if (texture.DX11)
{
var fileName = Path.GetFileName( path );
if( !fileName.StartsWith( "--" ) )
var fileName = Path.GetFileName(path);
if (!fileName.StartsWith("--"))
{
path = path.Replace( fileName, $"--{fileName}" );
path = path.Replace(fileName, $"--{fileName}");
addedDashes = true;
}
}
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 )
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)
{
texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath;
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
dataWasChanged = true;
}
return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path );
return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, newPath, path, path);
}
public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged )
public static FileSwap CreateShader(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string shaderName, ref bool dataWasChanged)
{
var path = $"shader/sm5/shpk/{shaderName}";
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path );
return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path);
}
public static FileSwap CreateAtex( Func< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged )
public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string filePath, ref bool dataWasChanged)
{
var oldPath = filePath;
filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}" );
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
dataWasChanged = true;
return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, oldPath );
return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath);
}
}
}

View file

@ -6,6 +6,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Services;
@ -27,7 +28,7 @@ public static class ItemSwap
=> Type = type;
}
private static bool LoadFile( FullPath path, out byte[] data )
private static bool LoadFile( MetaFileManager manager, FullPath path, out byte[] data )
{
if( path.FullName.Length > 0 )
{
@ -39,7 +40,7 @@ public static class ItemSwap
return true;
}
var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() );
var file = manager.GameData.GetFile( path.InternalName.ToString() );
if( file != null )
{
data = file.Data;
@ -61,18 +62,18 @@ public static class ItemSwap
public readonly byte[] Data;
public bool Valid { get; }
public GenericFile( FullPath path )
=> Valid = LoadFile( path, out Data );
public GenericFile( MetaFileManager manager, FullPath path )
=> Valid = LoadFile( manager, path, out Data );
public byte[] Write()
=> Data;
public static readonly GenericFile Invalid = new(FullPath.Empty);
public static readonly GenericFile Invalid = new(null!, FullPath.Empty);
}
public static bool LoadFile( FullPath path, [NotNullWhen( true )] out GenericFile? file )
public static bool LoadFile( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out GenericFile? file )
{
file = new GenericFile( path );
file = new GenericFile( manager, path );
if( file.Valid )
{
return true;
@ -82,11 +83,11 @@ public static class ItemSwap
return false;
}
public static bool LoadMdl( FullPath path, [NotNullWhen( true )] out MdlFile? file )
public static bool LoadMdl( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MdlFile? file )
{
try
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new MdlFile( data );
return true;
@ -101,11 +102,11 @@ public static class ItemSwap
return false;
}
public static bool LoadMtrl( FullPath path, [NotNullWhen( true )] out MtrlFile? file )
public static bool LoadMtrl(MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MtrlFile? file )
{
try
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new MtrlFile( data );
return true;
@ -120,11 +121,11 @@ public static class ItemSwap
return false;
}
public static bool LoadAvfx( FullPath path, [NotNullWhen( true )] out AvfxFile? file )
public static bool LoadAvfx( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out AvfxFile? file )
{
try
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new AvfxFile( data );
return true;
@ -140,20 +141,20 @@ public static class ItemSwap
}
public static FileSwap CreatePhyb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
public static FileSwap CreatePhyb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
{
var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry );
return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath );
return FileSwap.CreateSwap( manager, ResourceType.Phyb, redirections, phybPath, phybPath );
}
public static FileSwap CreateSklb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
public static FileSwap CreateSklb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
{
var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry );
return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath );
return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath );
}
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
public static MetaSwap? CreateEst( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type,
public static MetaSwap? CreateEst( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type,
GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl )
{
if( type == 0 )
@ -162,15 +163,15 @@ public static class ItemSwap
}
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 ) );
var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( manager, type, genderRace, idFrom.Value ) );
var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( manager, type, genderRace, idTo.Value ) );
var est = new MetaSwap( manips, fromDefault, toDefault );
if( ownMdl && est.SwapApplied.Est.Entry >= 2 )
{
var phyb = CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry );
var phyb = CreatePhyb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry );
est.ChildSwaps.Add( phyb );
var sklb = CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry );
var sklb = CreateSklb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry );
est.ChildSwaps.Add( sklb );
}
else if( est.SwapAppliedIsDefault )

View file

@ -9,12 +9,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData;
using Penumbra.Meta;
using Penumbra.Mods.Manager;
namespace Penumbra.Mods.ItemSwap;
public class ItemSwapContainer
{
private readonly MetaFileManager _manager;
private readonly IObjectIdentifier _identifier;
private Dictionary< Utf8GamePath, FullPath > _modRedirections = new();
@ -112,8 +114,9 @@ public class ItemSwapContainer
}
}
public ItemSwapContainer(IObjectIdentifier identifier)
public ItemSwapContainer(MetaFileManager manager, IObjectIdentifier identifier)
{
_manager = manager;
_identifier = identifier;
LoadMod( null, null );
}
@ -133,7 +136,7 @@ public class ItemSwapContainer
{
Swaps.Clear();
Loaded = false;
var ret = EquipmentSwap.CreateItemSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing );
var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing );
Loaded = true;
return ret;
}
@ -142,15 +145,15 @@ public class ItemSwapContainer
{
Swaps.Clear();
Loaded = false;
var ret = EquipmentSwap.CreateTypeSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to );
var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier, 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 )
public bool LoadCustomization( MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null )
{
var pathResolver = PathResolver( collection );
var mdl = CustomizationSwap.CreateMdl( pathResolver, slot, race, from, to );
var mdl = CustomizationSwap.CreateMdl( manager, pathResolver, slot, race, from, to );
var type = slot switch
{
BodySlot.Hair => EstManipulation.EstType.Hair,
@ -159,7 +162,7 @@ public class ItemSwapContainer
};
var metaResolver = MetaResolver( collection );
var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to, true );
var est = ItemSwap.CreateEst( manager, pathResolver, metaResolver, type, race, from, to, true );
Swaps.Add( mdl );
if( est != null )

View file

@ -7,18 +7,19 @@ using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using static Penumbra.Mods.ItemSwap.ItemSwap;
using Penumbra.Services;
using Penumbra.Services;
namespace Penumbra.Mods.ItemSwap;
public class Swap
{
/// <summary> Any further swaps belonging specifically to this tree of changes. </summary>
public readonly List< Swap > ChildSwaps = new();
public readonly List<Swap> ChildSwaps = new();
public IEnumerable< Swap > WithChildren()
=> ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this );
public IEnumerable<Swap> WithChildren()
=> ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this);
}
public sealed class MetaSwap : Swap
@ -47,15 +48,15 @@ public sealed class MetaSwap : Swap
/// <param name="manipulations">A function that converts the given manipulation to the modded one.</param>
/// <param name="manipFrom">The original meta identifier with its default value.</param>
/// <param name="manipTo">The target meta identifier with its default value.</param>
public MetaSwap( Func< MetaManipulation, MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo )
public MetaSwap(Func<MetaManipulation, MetaManipulation> manipulations, MetaManipulation manipFrom, MetaManipulation manipTo)
{
SwapFrom = manipFrom;
SwapToDefault = manipTo;
SwapToModded = manipulations( manipTo );
SwapToIsDefault = manipTo.EntryEquals( SwapToModded );
SwapApplied = SwapFrom.WithEntryOf( SwapToModded );
SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom );
SwapToModded = manipulations(manipTo);
SwapToIsDefault = manipTo.EntryEquals(SwapToModded);
SwapApplied = SwapFrom.WithEntryOf(SwapToModded);
SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom);
}
}
@ -95,8 +96,8 @@ public sealed class FileSwap : Swap
/// <summary> Whether SwapFromPreChangePath equals SwapFromRequest. </summary>
public bool SwapFromChanged;
public string GetNewPath( string newMod )
=> Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() );
public string GetNewPath(string newMod)
=> Path.Combine(newMod, new Utf8RelPath(SwapFromRequestPath).ToString());
public MdlFile? AsMdl()
=> FileData as MdlFile;
@ -116,8 +117,9 @@ public sealed class FileSwap : Swap
/// <param name="swapToRequest">The unmodded path to the file the game is supposed to load instead.</param>
/// <param name="swap">A full swap container with the actual file in memory.</param>
/// <returns>True if everything could be read correctly, false otherwise.</returns>
public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest,
string? swapFromPreChange = null )
public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func<Utf8GamePath, FullPath> redirections,
string swapFromRequest, string swapToRequest,
string? swapFromPreChange = null)
{
var swap = new FileSwap
{
@ -131,49 +133,25 @@ public sealed class FileSwap : Swap
SwapToModded = FullPath.Empty,
};
if( swapFromRequest.Length == 0
|| swapToRequest.Length == 0
|| !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath )
|| !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) )
{
throw new Exception( $"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"." );
}
if (swapFromRequest.Length == 0
|| swapToRequest.Length == 0
|| !Utf8GamePath.FromString(swapToRequest, out swap.SwapToRequestPath)
|| !Utf8GamePath.FromString(swapFromRequest, out swap.SwapFromRequestPath))
throw new Exception($"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\".");
swap.SwapToModded = redirections( swap.SwapToRequestPath );
swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() );
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path );
swap.SwapToModded = redirections(swap.SwapToRequestPath);
swap.SwapToModdedExistsInGame =
!swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists(swap.SwapToModded.InternalName.ToString());
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals(swap.SwapFromRequestPath.Path);
swap.FileData = type switch
{
ResourceType.Mdl => LoadMdl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ),
ResourceType.Mtrl => LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ),
ResourceType.Avfx => LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ),
_ => LoadFile( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ),
ResourceType.Mdl => LoadMdl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
ResourceType.Mtrl => LoadMtrl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
ResourceType.Avfx => LoadAvfx(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
_ => LoadFile(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
};
return swap;
}
/// <summary>
/// Convert a single file redirection to use the file name and extension given by type and the files SHA256 hash, if possible.
/// </summary>
/// <param name="redirections">The set of redirections that need to be considered.</param>
/// <param name="path">The in- and output path for a file</param>
/// <param name="dataWasChanged">Will be set to true if <paramref name="path"/> was changed.</param>
/// <param name="swap">Will be updated.</param>
public static bool CreateShaRedirection( Func< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap )
{
var oldFilename = Path.GetFileName( path );
var hash = SHA256.HashData( swap.FileData.Write() );
var name =
$"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}";
var newPath = path.Replace( oldFilename, name );
var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString() );
path = newPath;
dataWasChanged = true;
swap = newSwap;
return true;
}
}
}

View file

@ -5,6 +5,7 @@ using System.Linq;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
@ -109,11 +110,11 @@ public partial class Mod
}
}
public void WriteAllTexToolsMeta()
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
_default.WriteTexToolsMeta(ModPath);
_default.WriteTexToolsMeta(manager, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
@ -126,7 +127,7 @@ public partial class Mod
if (!optionDir.Exists)
optionDir.Create();
option.WriteTexToolsMeta(optionDir);
option.WriteTexToolsMeta(manager, optionDir);
}
}
}

View file

@ -5,12 +5,13 @@ using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using Penumbra.Import;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
/// <summary>
/// <summary>
/// A sub mod is a collection of
/// - file replacements
/// - file swaps
@ -18,14 +19,14 @@ namespace Penumbra.Mods;
/// that can be used either as an option or as the default data for a mod.
/// It can be loaded and reloaded from Json.
/// Nothing is checked for existence or validity when loading.
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// </summary>
public sealed class SubMod : ISubMod
public sealed class SubMod : ISubMod
{
public string Name { get; set; } = "Default";
public string FullName
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}";
public string Description { get; set; } = string.Empty;
@ -36,180 +37,165 @@ namespace Penumbra.Mods;
public bool IsDefault
=> GroupIdx < 0;
public Dictionary< Utf8GamePath, FullPath > FileData = new();
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
public HashSet< MetaManipulation > ManipulationData = new();
public Dictionary<Utf8GamePath, FullPath> FileData = new();
public Dictionary<Utf8GamePath, FullPath> FileSwapData = new();
public HashSet<MetaManipulation> ManipulationData = new();
public SubMod( IMod parentMod )
public SubMod(IMod parentMod)
=> ParentMod = parentMod;
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
=> FileData;
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
=> FileSwapData;
public IReadOnlySet< MetaManipulation > Manipulations
public IReadOnlySet<MetaManipulation> Manipulations
=> ManipulationData;
public void SetPosition( int groupIdx, int optionIdx )
public void SetPosition(int groupIdx, int optionIdx)
{
GroupIdx = groupIdx;
OptionIdx = optionIdx;
}
public void Load( DirectoryInfo basePath, JToken json, out int priority )
public void Load(DirectoryInfo basePath, JToken json, out int priority)
{
FileData.Clear();
FileSwapData.Clear();
ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options.
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
Name = json[nameof(ISubMod.Name)]?.ToObject<string>() ?? string.Empty;
Description = json[nameof(ISubMod.Description)]?.ToObject<string>() ?? string.Empty;
priority = json[nameof(IModGroup.Priority)]?.ToObject<int>() ?? 0;
var files = ( JObject? )json[ nameof( Files ) ];
if( files != null )
{
foreach( var property in files.Properties() )
var files = (JObject?)json[nameof(Files)];
if (files != null)
foreach (var property in files.Properties())
{
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
}
if (Utf8GamePath.FromString(property.Name, out var p, true))
FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
}
}
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
if( swaps != null )
{
foreach( var property in swaps.Properties() )
var swaps = (JObject?)json[nameof(FileSwaps)];
if (swaps != null)
foreach (var property in swaps.Properties())
{
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
}
if (Utf8GamePath.FromString(property.Name, out var p, true))
FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
}
}
var manips = json[ nameof( Manipulations ) ];
if( manips != null )
{
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
ManipulationData.Add( s );
}
}
var manips = json[nameof(Manipulations)];
if (manips != null)
foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
ManipulationData.Add(s);
}
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
// If delete is true, the files are deleted afterwards.
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(DirectoryInfo basePath, bool delete)
{
var deleteList = new List< string >();
var deleteList = new List<string>();
var oldSize = ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach( var (key, file) in Files.ToList() )
foreach (var (key, file) in Files.ToList())
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
{
if( ext1 == ".meta" || ext2 == ".meta" )
if (ext1 == ".meta" || ext2 == ".meta")
{
FileData.Remove( key );
if( !file.Exists )
{
FileData.Remove(key);
if (!file.Exists)
continue;
}
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( meta.MetaManipulations );
var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith(meta.MetaManipulations);
}
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
FileData.Remove( key );
if( !file.Exists )
{
FileData.Remove(key);
if (!file.Exists)
continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith( rgsp.MetaManipulations );
ManipulationData.UnionWith(rgsp.MetaManipulations);
}
}
catch( Exception e )
catch (Exception e)
{
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}");
}
}
DeleteDeleteList( deleteList, delete );
return ( oldSize < ManipulationData.Count, deleteList );
DeleteDeleteList(deleteList, delete);
return (oldSize < ManipulationData.Count, deleteList);
}
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete)
{
if( !delete )
{
if (!delete)
return;
}
foreach( var file in deleteList )
foreach (var file in deleteList)
{
try
{
File.Delete( file );
File.Delete(file);
}
catch( Exception e )
catch (Exception e)
{
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}");
}
}
}
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false)
{
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
var files = TexToolsMeta.ConvertToTexTools(manager, Manipulations);
foreach( var (file, data) in files )
foreach (var (file, data) in files)
{
var path = Path.Combine( basePath.FullName, file );
var path = Path.Combine(basePath.FullName, file);
try
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllBytes( path, data );
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllBytes(path, data);
}
catch( Exception e )
catch (Exception e)
{
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
Penumbra.Log.Error($"Could not write meta file {path}:\n{e}");
}
}
if( test )
{
TestMetaWriting( files );
}
if (test)
TestMetaWriting(manager, files);
}
[Conditional("DEBUG" )]
private void TestMetaWriting( Dictionary< string, byte[] > files )
[Conditional("DEBUG")]
private void TestMetaWriting(MetaFileManager manager, Dictionary<string, byte[]> files)
{
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
foreach( var (file, data) in files )
var meta = new HashSet<MetaManipulation>(Manipulations.Count);
foreach (var (file, data) in files)
{
try
{
var x = file.EndsWith( "rgsp" )
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
meta.UnionWith( x.MetaManipulations );
var x = file.EndsWith("rgsp")
? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges)
: new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges);
meta.UnionWith(x.MetaManipulations);
}
catch
{
@ -217,27 +203,21 @@ namespace Penumbra.Mods;
}
}
if( !Manipulations.SetEquals( meta ) )
if (!Manipulations.SetEquals(meta))
{
Penumbra.Log.Information( "Meta Sets do not equal." );
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
{
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
}
Penumbra.Log.Information("Meta Sets do not equal.");
foreach (var (m1, m2) in Manipulations.Zip(meta))
Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}");
foreach( var m in Manipulations.Skip( meta.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach (var m in Manipulations.Skip(meta.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
foreach( var m in meta.Skip( Manipulations.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach (var m in meta.Skip(Manipulations.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
}
else
{
Penumbra.Log.Information( "Meta Sets are equal." );
Penumbra.Log.Information("Meta Sets are equal.");
}
}
}
}