diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs new file mode 100644 index 00000000..910854b9 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Linq; +using System.Text; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public byte[] Write() + { + using var stream = new MemoryStream(); + using( var w = new BinaryWriter( stream ) ) + { + const int materialHeaderSize = 4 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1; + + w.BaseStream.Seek( materialHeaderSize, SeekOrigin.Begin ); + ushort cumulativeStringOffset = 0; + foreach( var texture in Textures ) + { + w.Write( cumulativeStringOffset ); + w.Write( texture.Flags ); + cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 ); + } + + foreach( var colorSet in UvColorSets.Concat( ColorSets ) ) + { + w.Write( cumulativeStringOffset ); + w.Write( colorSet.Index ); + cumulativeStringOffset += ( ushort )( colorSet.Name.Length + 1 ); + } + + foreach( var text in Textures.Select( t => t.Path ) + .Concat( UvColorSets.Concat( ColorSets ).Select( c => c.Name ).Append( ShaderPackage.Name ) ) ) + { + w.Write( Encoding.UTF8.GetBytes( text ) ); + w.Write( ( byte )'\0' ); + } + + w.Write( AdditionalData ); + foreach( var color in ColorSetData ) + { + w.Write( color ); + } + + w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); + w.Write( ( ushort )ShaderPackage.ShaderKeys.Length ); + w.Write( ( ushort )ShaderPackage.Constants.Length ); + w.Write( ( ushort )ShaderPackage.Samplers.Length ); + w.Write( ShaderPackage.Unk ); + + foreach( var key in ShaderPackage.ShaderKeys ) + { + w.Write( key.Category ); + w.Write( key.Value ); + } + + foreach( var constant in ShaderPackage.Constants ) + { + w.Write( constant.Id ); + w.Write( constant.Value ); + } + + foreach( var sampler in ShaderPackage.Samplers ) + { + w.Write( sampler.SamplerId ); + w.Write( sampler.Flags ); + w.Write( sampler.TextureIndex ); + w.Write( ( ushort )0 ); + w.Write( ( byte )0 ); + } + + foreach( var value in ShaderPackage.ShaderValues ) + { + w.Write( value ); + } + + WriteHeader( w, ( ushort )w.BaseStream.Position, cumulativeStringOffset ); + } + + return stream.ToArray(); + } + + private void WriteHeader( BinaryWriter w, ushort fileSize, ushort shaderPackageNameOffset ) + { + w.BaseStream.Seek( 0, SeekOrigin.Begin ); + w.Write( Version ); + w.Write( fileSize ); + w.Write( ( ushort )( ColorSetData.Length * 2 ) ); + w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); + w.Write( shaderPackageNameOffset ); + w.Write( ( byte )Textures.Length ); + w.Write( ( byte )UvColorSets.Length ); + w.Write( ( byte )ColorSets.Length ); + w.Write( ( byte )AdditionalData.Length ); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs new file mode 100644 index 00000000..5d6d81a0 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using System.Text; +using Lumina.Data.Parsing; +using Lumina.Extensions; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public struct ColorSet + { + public string Name; + public ushort Index; + } + + public struct Texture + { + public string Path; + public ushort Flags; + } + + public struct Constant + { + public uint Id; + public uint Value; + } + + public struct ShaderPackageData + { + public string Name; + public ShaderKey[] ShaderKeys; + public Constant[] Constants; + public Sampler[] Samplers; + public float[] ShaderValues; + public uint Unk; + } + + + public uint Version; + + public Texture[] Textures; + public ColorSet[] UvColorSets; + public ColorSet[] ColorSets; + public ushort[] ColorSetData; + public ShaderPackageData ShaderPackage; + public byte[] AdditionalData; + + public MtrlFile( byte[] data ) + { + using var stream = new MemoryStream( data ); + using var r = new BinaryReader( stream ); + + Version = r.ReadUInt32(); + r.ReadUInt16(); // file size + var dataSetSize = r.ReadUInt16(); + var stringTableSize = r.ReadUInt16(); + var shaderPackageNameOffset = r.ReadUInt16(); + var textureCount = r.ReadByte(); + var uvSetCount = r.ReadByte(); + var colorSetCount = r.ReadByte(); + var additionalDataSize = r.ReadByte(); + + Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); + UvColorSets = ReadColorSetOffsets( r, uvSetCount, out var uvOffsets ); + ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + + var strings = r.ReadBytes( stringTableSize ); + for( var i = 0; i < textureCount; ++i ) + { + Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] ); + } + + for( var i = 0; i < uvSetCount; ++i ) + { + UvColorSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); + } + + for( var i = 0; i < colorSetCount; ++i ) + { + ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); + } + + ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); + + AdditionalData = r.ReadBytes( additionalDataSize ); + ColorSetData = r.ReadStructuresAsArray< ushort >( dataSetSize / 2 ); + + var shaderValueListSize = r.ReadUInt16(); + var shaderKeyCount = r.ReadUInt16(); + var constantCount = r.ReadUInt16(); + var samplerCount = r.ReadUInt16(); + ShaderPackage.Unk = r.ReadUInt32(); + + ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount ); + ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount ); + ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount ); + ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 ); + } + + private static Texture[] ReadTextureOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new Texture[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Flags = r.ReadUInt16(); + } + + return ret; + } + + private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new ColorSet[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Index = r.ReadUInt16(); + } + + return ret; + } + + private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset ) + { + strings = strings[ offset.. ]; + var end = strings.IndexOf( ( byte )'\0' ); + return Encoding.UTF8.GetString( strings[ ..end ] ); + } +} \ No newline at end of file