using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Lumina.Data.Parsing; namespace Penumbra.GameData.Files; public partial class MdlFile { private static uint Write( BinaryWriter w, string s, long basePos ) { var currentPos = w.BaseStream.Position; w.Write( Encoding.UTF8.GetBytes( s ) ); w.Write( ( byte )0 ); return ( uint )( currentPos - basePos ); } private List< uint > WriteStrings( BinaryWriter w ) { var startPos = ( int )w.BaseStream.Position; var basePos = startPos + 8; var count = ( ushort )( Attributes.Length + Bones.Length + Materials.Length + Shapes.Length ); w.Write( count ); w.Seek( basePos, SeekOrigin.Begin ); var ret = Attributes.Concat( Bones ) .Concat( Materials ) .Concat( Shapes.Select( s => s.ShapeName ) ) .Select( attribute => Write( w, attribute, basePos ) ).ToList(); w.Write( ( ushort )0 ); // Seems to always have two additional null-bytes, not padding. var size = ( int )w.BaseStream.Position - basePos; w.Seek( startPos + 4, SeekOrigin.Begin ); w.Write( ( uint )size ); w.Seek( basePos + size, SeekOrigin.Begin ); return ret; } private void WriteModelFileHeader( BinaryWriter w, uint runtimeSize ) { w.Write( Version ); w.Write( StackSize ); w.Write( runtimeSize ); w.Write( ( ushort )VertexDeclarations.Length ); w.Write( ( ushort )Materials.Length ); w.Write( VertexOffset[ 0 ] > 0 ? VertexOffset[ 0 ] + runtimeSize : 0u ); w.Write( VertexOffset[ 1 ] > 0 ? VertexOffset[ 1 ] + runtimeSize : 0u ); w.Write( VertexOffset[ 2 ] > 0 ? VertexOffset[ 2 ] + runtimeSize : 0u ); w.Write( IndexOffset[ 0 ] > 0 ? IndexOffset[ 0 ] + runtimeSize : 0u ); w.Write( IndexOffset[ 1 ] > 0 ? IndexOffset[ 1 ] + runtimeSize : 0u ); w.Write( IndexOffset[ 2 ] > 0 ? IndexOffset[ 2 ] + runtimeSize : 0u ); w.Write( VertexBufferSize[ 0 ] ); w.Write( VertexBufferSize[ 1 ] ); w.Write( VertexBufferSize[ 2 ] ); w.Write( IndexBufferSize[ 0 ] ); w.Write( IndexBufferSize[ 1 ] ); w.Write( IndexBufferSize[ 2 ] ); w.Write( LodCount ); w.Write( EnableIndexBufferStreaming ); w.Write( EnableEdgeGeometry ); w.Write( ( byte )0 ); // Padding } private void WriteModelHeader( BinaryWriter w ) { w.Write( Radius ); w.Write( ( ushort )Meshes.Length ); w.Write( ( ushort )Attributes.Length ); w.Write( ( ushort )SubMeshes.Length ); w.Write( ( ushort )Materials.Length ); w.Write( ( ushort )Bones.Length ); w.Write( ( ushort )BoneTables.Length ); w.Write( ( ushort )Shapes.Length ); w.Write( ( ushort )ShapeMeshes.Length ); w.Write( ( ushort )ShapeValues.Length ); w.Write( LodCount ); w.Write( ( byte )Flags1 ); w.Write( ( ushort )ElementIds.Length ); w.Write( ( byte )TerrainShadowMeshes.Length ); w.Write( ( byte )Flags2 ); w.Write( ModelClipOutDistance ); w.Write( ShadowClipOutDistance ); w.Write( Unknown4 ); w.Write( ( ushort )TerrainShadowSubMeshes.Length ); w.Write( Unknown5 ); w.Write( BgChangeMaterialIndex ); w.Write( BgCrestChangeMaterialIndex ); w.Write( Unknown6 ); w.Write( Unknown7 ); w.Write( Unknown8 ); w.Write( Unknown9 ); w.Write( ( uint )0 ); // 6 byte padding w.Write( ( ushort )0 ); } private static void Write( BinaryWriter w, in MdlStructs.VertexElement vertex ) { w.Write( vertex.Stream ); w.Write( vertex.Offset ); w.Write( vertex.Type ); w.Write( vertex.Usage ); w.Write( vertex.UsageIndex ); w.Write( ( ushort )0 ); // 3 byte padding w.Write( ( byte )0 ); } private static void Write( BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl ) { foreach( var vertex in vertexDecl.VertexElements ) { Write( w, vertex ); } Write( w, new MdlStructs.VertexElement() { Stream = 255 } ); w.Seek( ( int )( NumVertices - 1 - vertexDecl.VertexElements.Length ) * 8, SeekOrigin.Current ); } private static void Write( BinaryWriter w, in MdlStructs.ElementIdStruct elementId ) { w.Write( elementId.ElementId ); w.Write( elementId.ParentBoneName ); w.Write( elementId.Translate[ 0 ] ); w.Write( elementId.Translate[ 1 ] ); w.Write( elementId.Translate[ 2 ] ); w.Write( elementId.Rotate[ 0 ] ); w.Write( elementId.Rotate[ 1 ] ); w.Write( elementId.Rotate[ 2 ] ); } private static unsafe void Write< T >( BinaryWriter w, in T data ) where T : unmanaged { fixed( T* ptr = &data ) { var bytePtr = ( byte* )ptr; var size = sizeof( T ); var span = new ReadOnlySpan< byte >( bytePtr, size ); w.Write( span ); } } private static void Write( BinaryWriter w, MdlStructs.MeshStruct mesh ) { w.Write( mesh.VertexCount ); w.Write( ( ushort )0 ); // padding w.Write( mesh.IndexCount ); w.Write( mesh.MaterialIndex ); w.Write( mesh.SubMeshIndex ); w.Write( mesh.SubMeshCount ); w.Write( mesh.BoneTableIndex ); w.Write( mesh.StartIndex ); w.Write( mesh.VertexBufferOffset[ 0 ] ); w.Write( mesh.VertexBufferOffset[ 1 ] ); w.Write( mesh.VertexBufferOffset[ 2 ] ); w.Write( mesh.VertexBufferStride[ 0 ] ); w.Write( mesh.VertexBufferStride[ 1 ] ); w.Write( mesh.VertexBufferStride[ 2 ] ); w.Write( mesh.VertexStreamCount ); } private static void Write( BinaryWriter w, MdlStructs.BoneTableStruct bone ) { foreach( var index in bone.BoneIndex ) { w.Write( index ); } w.Write( bone.BoneCount ); w.Write( ( ushort )0 ); // 3 bytes padding w.Write( ( byte )0 ); } private void Write( BinaryWriter w, int shapeIdx, IReadOnlyList< uint > offsets ) { var shape = Shapes[ shapeIdx ]; var offset = offsets[ Attributes.Length + Bones.Length + Materials.Length + shapeIdx ]; w.Write( offset ); w.Write( shape.ShapeMeshStartIndex[ 0 ] ); w.Write( shape.ShapeMeshStartIndex[ 1 ] ); w.Write( shape.ShapeMeshStartIndex[ 2 ] ); w.Write( shape.ShapeMeshCount[ 0 ] ); w.Write( shape.ShapeMeshCount[ 1 ] ); w.Write( shape.ShapeMeshCount[ 2 ] ); } private static void Write( BinaryWriter w, MdlStructs.BoundingBoxStruct box ) { w.Write( box.Min[ 0 ] ); w.Write( box.Min[ 1 ] ); w.Write( box.Min[ 2 ] ); w.Write( box.Min[ 3 ] ); w.Write( box.Max[ 0 ] ); w.Write( box.Max[ 1 ] ); w.Write( box.Max[ 2 ] ); w.Write( box.Max[ 3 ] ); } public byte[] Write() { using var stream = new MemoryStream(); using( var w = new BinaryWriter( stream ) ) { // Skip and write this later when we actually know it. w.Seek( ( int )FileHeaderSize, SeekOrigin.Begin ); foreach( var vertexDecl in VertexDeclarations ) { Write( w, vertexDecl ); } var offsets = WriteStrings( w ); WriteModelHeader( w ); foreach( var elementId in ElementIds ) { Write( w, elementId ); } foreach( var lod in Lods ) { Write( w, lod ); } if( Flags2.HasFlag( MdlStructs.ModelFlags2.ExtraLodEnabled ) ) { foreach( var extraLod in ExtraLods ) { Write( w, extraLod ); } } foreach( var mesh in Meshes ) { Write( w, mesh ); } for( var i = 0; i < Attributes.Length; ++i ) { w.Write( offsets[ i ] ); } foreach( var terrainShadowMesh in TerrainShadowMeshes ) { Write( w, terrainShadowMesh ); } foreach( var subMesh in SubMeshes ) { Write( w, subMesh ); } foreach( var terrainShadowSubMesh in TerrainShadowSubMeshes ) { Write( w, terrainShadowSubMesh ); } for( var i = 0; i < Materials.Length; ++i ) { w.Write( offsets[ Attributes.Length + Bones.Length + i ] ); } for( var i = 0; i < Bones.Length; ++i ) { w.Write( offsets[ Attributes.Length + i ] ); } foreach( var boneTable in BoneTables ) { Write( w, boneTable ); } for( var i = 0; i < Shapes.Length; ++i ) { Write( w, i, offsets ); } foreach( var shapeMesh in ShapeMeshes ) { Write( w, shapeMesh ); } foreach( var shapeValue in ShapeValues ) { Write( w, shapeValue ); } w.Write( SubMeshBoneMap.Length * 2 ); foreach( var bone in SubMeshBoneMap ) { w.Write( bone ); } w.Write( ( byte )0 ); // number of padding bytes, which is 0 for us. Write( w, BoundingBoxes ); Write( w, ModelBoundingBoxes ); Write( w, WaterBoundingBoxes ); Write( w, VerticalFogBoundingBoxes ); foreach( var box in BoneBoundingBoxes ) { Write( w, box ); } var totalSize = w.BaseStream.Position; var runtimeSize = ( uint )( totalSize - StackSize - FileHeaderSize ); w.Write( RemainingData ); // Write header data. w.Seek( 0, SeekOrigin.Begin ); WriteModelFileHeader( w, runtimeSize ); } return stream.ToArray(); } }