diff --git a/Penumbra.GameData/Files/MdlFile.Write.cs b/Penumbra.GameData/Files/MdlFile.Write.cs new file mode 100644 index 00000000..b4017c52 --- /dev/null +++ b/Penumbra.GameData/Files/MdlFile.Write.cs @@ -0,0 +1,317 @@ +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(); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs new file mode 100644 index 00000000..cf829a65 --- /dev/null +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -0,0 +1,259 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Lumina.Data.Parsing; +using Lumina.Extensions; + +namespace Penumbra.GameData.Files; + +public partial class MdlFile +{ + public const uint NumVertices = 17; + public const uint FileHeaderSize = 0x44; + + // Refers to string, thus not Lumina struct. + public struct Shape + { + public string ShapeName = string.Empty; + public ushort[] ShapeMeshStartIndex; + public ushort[] ShapeMeshCount; + + public Shape( MdlStructs.ShapeStruct data, uint[] offsets, string[] strings ) + { + var idx = offsets.AsSpan().IndexOf( data.StringOffset ); + ShapeName = idx >= 0 ? strings[ idx ] : string.Empty; + ShapeMeshStartIndex = data.ShapeMeshStartIndex; + ShapeMeshCount = data.ShapeMeshCount; + } + } + + // Raw data to write back. + public uint Version; + public float Radius; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public byte BgChangeMaterialIndex; + public byte BgCrestChangeMaterialIndex; + public ushort Unknown4; + public byte Unknown5; + public byte Unknown6; + public ushort Unknown7; + public ushort Unknown8; + public ushort Unknown9; + + // Offsets are stored relative to RuntimeSize instead of file start. + public uint[] VertexOffset; + public uint[] IndexOffset; + + public uint[] VertexBufferSize; + public uint[] IndexBufferSize; + public byte LodCount; + public bool EnableIndexBufferStreaming; + public bool EnableEdgeGeometry; + + + public MdlStructs.ModelFlags1 Flags1; + public MdlStructs.ModelFlags2 Flags2; + + public MdlStructs.BoundingBoxStruct BoundingBoxes; + public MdlStructs.BoundingBoxStruct ModelBoundingBoxes; + public MdlStructs.BoundingBoxStruct WaterBoundingBoxes; + public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes; + + public MdlStructs.VertexDeclarationStruct[] VertexDeclarations; + public MdlStructs.ElementIdStruct[] ElementIds; + public MdlStructs.MeshStruct[] Meshes; + public MdlStructs.BoneTableStruct[] BoneTables; + public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes; + public MdlStructs.SubmeshStruct[] SubMeshes; + public MdlStructs.ShapeMeshStruct[] ShapeMeshes; + public MdlStructs.ShapeValueStruct[] ShapeValues; + public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes; + public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubMeshes; + public MdlStructs.LodStruct[] Lods; + public MdlStructs.ExtraLodStruct[] ExtraLods; + public ushort[] SubMeshBoneMap; + + // Strings are written in order + public string[] Attributes; + public string[] Bones; + public string[] Materials; + public Shape[] Shapes; + + // Raw, unparsed data. + public byte[] RemainingData; + + public MdlFile( byte[] data ) + { + using var stream = new MemoryStream( data ); + using var r = new BinaryReader( stream ); + + var header = LoadModelFileHeader( r ); + LodCount = header.LodCount; + VertexBufferSize = header.VertexBufferSize; + IndexBufferSize = header.IndexBufferSize; + VertexOffset = header.VertexOffset; + IndexOffset = header.IndexOffset; + for( var i = 0; i < 3; ++i ) + { + if( VertexOffset[ i ] > 0 ) + { + VertexOffset[ i ] -= header.RuntimeSize; + } + + if( IndexOffset[ i ] > 0 ) + { + IndexOffset[ i ] -= header.RuntimeSize; + } + } + + VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount]; + for( var i = 0; i < header.VertexDeclarationCount; ++i ) + { + VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( r ); + } + + var (offsets, strings) = LoadStrings( r ); + + var modelHeader = LoadModelHeader( r ); + ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount]; + for( var i = 0; i < modelHeader.ElementIdCount; i++ ) + { + ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r ); + } + + Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 ); + ExtraLods = modelHeader.ExtraLodEnabled + ? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 ) + : Array.Empty< MdlStructs.ExtraLodStruct >(); + + Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount]; + for( var i = 0; i < modelHeader.MeshCount; i++ ) + { + Meshes[ i ] = MdlStructs.MeshStruct.Read( r ); + } + + Attributes = new string[modelHeader.AttributeCount]; + for( var i = 0; i < modelHeader.AttributeCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount ); + SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount ); + TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount ); + + Materials = new string[modelHeader.MaterialCount]; + for( var i = 0; i < modelHeader.MaterialCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + Bones = new string[modelHeader.BoneCount]; + for( var i = 0; i < modelHeader.BoneCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount]; + for( var i = 0; i < modelHeader.BoneTableCount; i++ ) + { + BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r ); + } + + Shapes = new Shape[modelHeader.ShapeCount]; + for( var i = 0; i < modelHeader.ShapeCount; i++ ) + { + Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings ); + } + + ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount ); + ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount ); + + var submeshBoneMapSize = r.ReadUInt32(); + SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray(); + + var paddingAmount = r.ReadByte(); + r.Seek( r.BaseStream.Position + paddingAmount ); + + // Dunno what this first one is for? + BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount]; + for( var i = 0; i < modelHeader.BoneCount; i++ ) + { + BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( r ); + } + + RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) ); + } + + private MdlStructs.ModelFileHeader LoadModelFileHeader( BinaryReader r ) + { + var header = MdlStructs.ModelFileHeader.Read( r ); + Version = header.Version; + EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; + EnableEdgeGeometry = header.EnableEdgeGeometry; + return header; + } + + private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r ) + { + var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >(); + Radius = modelHeader.Radius; + Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType() + .GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType() + .GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + ModelClipOutDistance = modelHeader.ModelClipOutDistance; + ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; + Unknown4 = modelHeader.Unknown4; + Unknown5 = ( byte )( modelHeader.GetType() + .GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + Unknown6 = modelHeader.Unknown6; + Unknown7 = modelHeader.Unknown7; + Unknown8 = modelHeader.Unknown8; + Unknown9 = modelHeader.Unknown9; + BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; + BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; + + return modelHeader; + } + + private static (uint[], string[]) LoadStrings( BinaryReader r ) + { + var stringCount = r.ReadUInt16(); + r.ReadUInt16(); + var stringSize = ( int )r.ReadUInt32(); + var stringData = r.ReadBytes( stringSize ); + var start = 0; + var strings = new string[stringCount]; + var offsets = new uint[stringCount]; + for( var i = 0; i < stringCount; ++i ) + { + var span = stringData.AsSpan( start ); + var idx = span.IndexOf( ( byte )'\0' ); + strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] ); + offsets[ i ] = ( uint )start; + start = start + idx + 1; + } + + return ( offsets, strings ); + } + + public unsafe uint StackSize + => ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) ); + +} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 9938007f..7cb1f068 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -1,41 +1,18 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Windows.Forms; using Dalamud.Logging; +using Penumbra.GameData.Files; using Penumbra.Mod; namespace Penumbra.Util; public static class ModelChanger { - private static int FindSubSequence( byte[] main, byte[] sub, int from = 0 ) - { - if( sub.Length + from > main.Length ) - { - return -1; - } - - var length = main.Length - sub.Length; - for( var i = from; i < length; ++i ) - { - var span = main.AsSpan( i, sub.Length ); - if( span.SequenceEqual( sub ) ) - { - return i; - } - } - - return -1; - } - - private static bool ConvertString( string text, out byte[] data ) - { - data = Encoding.UTF8.GetBytes( text ); - return data.Length == text.Length && !data.Any( b => b > 0b10000000 ); - } + public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; public static bool ValidStrings( string from, string to ) => from.Length != 0 @@ -46,88 +23,12 @@ public static class ModelChanger && Encoding.UTF8.GetByteCount( from ) == from.Length && Encoding.UTF8.GetByteCount( to ) == to.Length; - private static bool ConvertName( string name, out byte[] data ) - { - if( name.Length != 0 ) - { - return ConvertString( $"/mt_c0201b0001_{name}.mtrl", out data ); - } - data = Array.Empty< byte >(); - return false; - } + [Conditional( "Debug" )] + private static void WriteBackup( string name, byte[] text ) + => File.WriteAllBytes( name + ".bak", text ); - private static int ReplaceEqualSequences( byte[] main, byte[] subLhs, byte[] subRhs ) - { - if( subLhs.SequenceEqual( subRhs ) ) - { - return 0; - } - - var i = 0; - var replacements = 0; - while( ( i = FindSubSequence( main, subLhs, i ) ) > 0 ) - { - subRhs.CopyTo( main.AsSpan( i ) ); - i += subLhs.Length; - ++replacements; - } - - return replacements; - } - - private static void ReplaceOffsetsAndSizes( byte[] main, int sizeDiff ) - { - var stackSize = BitConverter.ToUInt32( main, 4 ); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( main, ( int )stringsLengthOffset ); - - BitConverter.TryWriteBytes( main.AsSpan( 8 ), ( uint )( BitConverter.ToUInt32( main, 8 ) + sizeDiff ) ); // RuntimeSize - BitConverter.TryWriteBytes( main.AsSpan( 16 ), ( uint )( BitConverter.ToUInt32( main, 16 ) + sizeDiff ) ); // VertexOffset 1 - BitConverter.TryWriteBytes( main.AsSpan( 20 ), ( uint )( BitConverter.ToUInt32( main, 20 ) + sizeDiff ) ); // VertexOffset 2 - BitConverter.TryWriteBytes( main.AsSpan( 24 ), ( uint )( BitConverter.ToUInt32( main, 24 ) + sizeDiff ) ); // VertexOffset 3 - BitConverter.TryWriteBytes( main.AsSpan( 28 ), ( uint )( BitConverter.ToUInt32( main, 28 ) + sizeDiff ) ); // IndexOffset 1 - BitConverter.TryWriteBytes( main.AsSpan( 32 ), ( uint )( BitConverter.ToUInt32( main, 32 ) + sizeDiff ) ); // IndexOffset 2 - BitConverter.TryWriteBytes( main.AsSpan( 36 ), ( uint )( BitConverter.ToUInt32( main, 36 ) + sizeDiff ) ); // IndexOffset 3 - BitConverter.TryWriteBytes( main.AsSpan( ( int )stringsLengthOffset ), ( uint )( stringsLength + sizeDiff ) ); - } - - private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs ) - { - if( subLhs.Length == subRhs.Length ) - { - return ReplaceEqualSequences( main, subLhs, subRhs ); - } - - var replacements = new List< int >( 4 ); - for( var i = FindSubSequence( main, subLhs ); i >= 0; i = FindSubSequence( main, subLhs, i + subLhs.Length ) ) - { - replacements.Add( i ); - } - - var sizeDiff = ( subRhs.Length - subLhs.Length ) * replacements.Count; - var ret = new byte[main.Length + sizeDiff]; - - var last = 0; - var totalLength = 0; - foreach( var i in replacements ) - { - var length = i - last; - main.AsSpan( last, length ).CopyTo( ret.AsSpan( totalLength ) ); - totalLength += length; - subRhs.CopyTo( ret.AsSpan( totalLength ) ); - totalLength += subRhs.Length; - last = i + subLhs.Length; - } - - main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) ); - ReplaceOffsetsAndSizes( ret, sizeDiff ); - main = ret; - return replacements.Count; - } - - public static int ChangeMtrl( FullPath file, byte[] from, byte[] to ) + public static int ChangeMtrl( FullPath file, string from, string to ) { if( !file.Exists ) { @@ -136,11 +37,25 @@ public static class ModelChanger try { - var text = File.ReadAllBytes( file.FullName ); - var replaced = ReplaceSubSequences( ref text, from, to ); + var data = File.ReadAllBytes( file.FullName ); + var mdlFile = new MdlFile( data ); + from = string.Format( MaterialFormat, from ); + to = string.Format( MaterialFormat, to ); + + var replaced = 0; + for( var i = 0; i < mdlFile.Materials.Length; ++i ) + { + if( mdlFile.Materials[ i ] == @from ) + { + mdlFile.Materials[ i ] = to; + ++replaced; + } + } + if( replaced > 0 ) { - File.WriteAllBytes( file.FullName, text ); + WriteBackup( file.FullName, data ); + File.WriteAllBytes( file.FullName, mdlFile.Write() ); } return replaced; @@ -154,11 +69,11 @@ public static class ModelChanger public static bool ChangeModMaterials( ModData mod, string from, string to ) { - if( ValidStrings( from, to ) && ConvertName( from, out var lhs ) && ConvertName( to, out var rhs ) ) + if( ValidStrings( from, to ) ) { return mod.Resources.ModFiles .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) - .All( file => ChangeMtrl( file, lhs, rhs ) >= 0 ); + .All( file => ChangeMtrl( file, from, to ) >= 0 ); } PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." );