mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-02 13:53:42 +01:00
Add equipment swaps and writing to option.
This commit is contained in:
parent
33b4905ae2
commit
ab53f17a7e
9 changed files with 723 additions and 526 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue