mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-02 13:53:42 +01:00
Add equipment swapping.
This commit is contained in:
parent
6cd43aa304
commit
a01f73cde4
10 changed files with 710 additions and 184 deletions
|
|
@ -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 );
|
||||
}
|
||||
|
||||
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
|
||||
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 );
|
||||
}
|
||||
}
|
||||
406
Penumbra/Mods/ItemSwap/EquipmentSwap.cs
Normal file
406
Penumbra/Mods/ItemSwap/EquipmentSwap.cs
Normal file
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
|
||||
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
|
||||
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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary> Any further swaps belonging specifically to this tree of changes. </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue