Add equipment swaps and writing to option.

This commit is contained in:
Ottermandias 2022-12-31 21:56:25 +01:00
parent 33b4905ae2
commit ab53f17a7e
9 changed files with 723 additions and 526 deletions

View file

@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
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;
namespace Penumbra.Mods.ItemSwap;
public class EquipmentDataContainer
{
public Item Item;
public EquipSlot Slot;
public SetId ModelId;
public byte Variant;
public ImcManipulation ImcData;
public EqpManipulation EqpData;
public GmpManipulation GmpData;
// Example: Abyssos Helm / Body
public string AvfxPath = string.Empty;
// Example: Dodore Doublet, but unknown what it does?
public string SoundPath = string.Empty;
// Example: Crimson Standard Bracelet
public string DecalPath = string.Empty;
// Example: The Howling Spirit and The Wailing Spirit, but unknown what it does.
public string AnimationPath = string.Empty;
public Dictionary< GenderRace, GenderRaceContainer > Files = new();
public struct GenderRaceContainer
{
public EqdpManipulation Eqdp;
public GenderRace ModelRace;
public GenderRace MaterialRace;
public EstManipulation Est;
public string MdlPath;
public MtrlContainer[] MtrlPaths;
}
public struct MtrlContainer
{
public string MtrlPath;
public string[] Textures;
public string Shader;
public MtrlContainer( string mtrlPath )
{
MtrlPath = mtrlPath;
var file = Dalamud.GameData.GetFile( mtrlPath );
if( file != null )
{
var mtrl = new MtrlFile( file.Data );
Textures = mtrl.Textures.Select( t => t.Path ).ToArray();
Shader = $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}";
}
else
{
Textures = Array.Empty< string >();
Shader = string.Empty;
}
}
}
private static EstManipulation GetEstEntry( GenderRace genderRace, SetId setId, EquipSlot slot )
{
if( slot == EquipSlot.Head )
{
var entry = EstFile.GetDefault( EstManipulation.EstType.Head, genderRace, setId.Value );
return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Head, setId.Value, entry );
}
if( slot == EquipSlot.Body )
{
var entry = EstFile.GetDefault( EstManipulation.EstType.Body, genderRace, setId.Value );
return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Body, setId.Value, entry );
}
return default;
}
private static GenderRaceContainer GetGenderRace( GenderRace genderRace, SetId modelId, EquipSlot slot, ushort materialId )
{
var ret = new GenderRaceContainer()
{
Eqdp = GetEqdpEntry( genderRace, modelId, slot ),
Est = GetEstEntry( genderRace, modelId, slot ),
};
( ret.ModelRace, ret.MaterialRace ) = TraverseEqdpTree( genderRace, modelId, slot );
ret.MdlPath = GamePaths.Equipment.Mdl.Path( modelId, ret.ModelRace, slot );
ret.MtrlPaths = MtrlPaths( ret.MdlPath, ret.MaterialRace, modelId, materialId );
return ret;
}
private static EqdpManipulation GetEqdpEntry( GenderRace genderRace, SetId modelId, EquipSlot slot )
{
var entry = ExpandedEqdpFile.GetDefault( genderRace, slot.IsAccessory(), modelId.Value );
return new EqdpManipulation( entry, slot, genderRace.Split().Item1, genderRace.Split().Item2, modelId.Value );
}
private static MtrlContainer[] MtrlPaths( string mdlPath, GenderRace mtrlRace, SetId modelId, ushort materialId )
{
var file = Dalamud.GameData.GetFile( mdlPath );
if( file == null )
{
return Array.Empty< MtrlContainer >();
}
var mdl = new MdlFile( Dalamud.GameData.GetFile( mdlPath )!.Data );
var basePath = GamePaths.Equipment.Mtrl.FolderPath( modelId, ( byte )materialId );
var equipPart = $"e{modelId.Value:D4}";
var racePart = $"c{mtrlRace.ToRaceCode()}";
return mdl.Materials
.Where( m => m.Contains( equipPart ) )
.Select( m => new MtrlContainer( $"{basePath}{m.Replace( "c0101", racePart )}" ) )
.ToArray();
}
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 );
}
public EquipmentDataContainer( Item i )
{
Item = i;
LookupItem( i, out Slot, out ModelId, out Variant );
LookupImc( ModelId, Variant, Slot );
EqpData = new EqpManipulation( ExpandedEqpFile.GetDefault( ModelId.Value ), Slot, ModelId.Value );
GmpData = Slot == EquipSlot.Head ? new GmpManipulation( ExpandedGmpFile.GetDefault( ModelId.Value ), ModelId.Value ) : default;
foreach( var genderRace in Enum.GetValues< GenderRace >() )
{
if( CharacterUtility.EqdpIdx( genderRace, Slot.IsAccessory() ) < 0 )
{
continue;
}
Files[ genderRace ] = GetGenderRace( genderRace, ModelId, Slot, ImcData.Entry.MaterialId );
}
}
private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant )
{
slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
if( !slot.IsEquipment() )
{
throw new ItemSwap.InvalidItemTypeException();
}
modelId = ( ( Quad )i.ModelMain ).A;
variant = ( byte )( ( Quad )i.ModelMain ).B;
}
private void LookupImc( SetId modelId, byte variant, EquipSlot slot )
{
var imc = ImcFile.GetDefault( GamePaths.Equipment.Imc.Path( modelId ), slot, variant, out var exists );
if( !exists )
{
throw new ItemSwap.InvalidImcException();
}
ImcData = new ImcManipulation( slot, variant, modelId.Value, imc );
if( imc.DecalId != 0 )
{
DecalPath = GamePaths.Equipment.Decal.Path( imc.DecalId );
}
// TODO: Figure out how this works.
if( imc.SoundId != 0 )
{
SoundPath = string.Empty;
}
if( imc.VfxId != 0 )
{
AvfxPath = GamePaths.Equipment.Avfx.Path( modelId, imc.VfxId );
}
// TODO: Figure out how this works.
if( imc.MaterialAnimationId != 0 )
{
AnimationPath = string.Empty;
}
}
}

View file

@ -252,9 +252,13 @@ public static class EquipmentSwap
return false;
}
// 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.
return true;
}
// Example: Crimson Standard Bracelet
public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc )
{
if( decalId != 0 )
@ -271,6 +275,8 @@ public static class EquipmentSwap
return true;
}
// Example: Abyssos Helm / Body
public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc )
{
if( vfxId != 0 )

View file

@ -37,11 +37,12 @@ public class ItemSwapContainer
NoSwaps,
}
public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps )
public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 )
{
var convertedManips = new HashSet< MetaManipulation >( Swaps.Count );
var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
directory ??= mod.ModPath;
try
{
foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) )
@ -62,7 +63,7 @@ public class ItemSwapContainer
}
else
{
var path = file.GetNewPath( mod.ModPath.FullName );
var path = file.GetNewPath( directory.FullName );
var bytes = file.FileData.Write();
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllBytes( path, bytes );
@ -80,9 +81,9 @@ public class ItemSwapContainer
}
}
Penumbra.ModManager.OptionSetFiles( mod, -1, 0, convertedFiles );
Penumbra.ModManager.OptionSetFileSwaps( mod, -1, 0, convertedSwaps );
Penumbra.ModManager.OptionSetManipulations( mod, -1, 0, convertedManips );
Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles );
Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps );
Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips );
return true;
}
catch( Exception e )
@ -120,7 +121,7 @@ public class ItemSwapContainer
Loaded = true;
return ret;
}
catch( Exception e )
catch
{
Swaps.Clear();
Loaded = false;

View file

@ -47,8 +47,10 @@ public partial class Mod
return new DirectoryInfo( newModFolder );
}
// Create the name for a group or option subfolder based on its parent folder and given name.
// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
/// <summary>
/// Create the name for a group or option subfolder based on its parent folder and given name.
/// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
/// </summary>
internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName )
{
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );