Fix material change in mdls another time, this time with actual .mdl parsing and writing.

This commit is contained in:
Ottermandias 2022-02-21 19:47:44 +01:00
parent 7beee50fdf
commit 733b60faae
3 changed files with 602 additions and 111 deletions

View file

@ -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();
}
}

View file

@ -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 ) );
}

View file

@ -1,41 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Windows.Forms;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.Files;
using Penumbra.Mod; using Penumbra.Mod;
namespace Penumbra.Util; namespace Penumbra.Util;
public static class ModelChanger public static class ModelChanger
{ {
private static int FindSubSequence( byte[] main, byte[] sub, int from = 0 ) public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl";
{
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 static bool ValidStrings( string from, string to ) public static bool ValidStrings( string from, string to )
=> from.Length != 0 => from.Length != 0
@ -46,88 +23,12 @@ public static class ModelChanger
&& Encoding.UTF8.GetByteCount( from ) == from.Length && Encoding.UTF8.GetByteCount( from ) == from.Length
&& Encoding.UTF8.GetByteCount( to ) == to.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 >(); [Conditional( "Debug" )]
return false; private static void WriteBackup( string name, byte[] text )
} => File.WriteAllBytes( name + ".bak", text );
private static int ReplaceEqualSequences( byte[] main, byte[] subLhs, byte[] subRhs ) public static int ChangeMtrl( FullPath file, string from, string to )
{
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 )
{ {
if( !file.Exists ) if( !file.Exists )
{ {
@ -136,11 +37,25 @@ public static class ModelChanger
try try
{ {
var text = File.ReadAllBytes( file.FullName ); var data = File.ReadAllBytes( file.FullName );
var replaced = ReplaceSubSequences( ref text, from, to ); 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 ) if( replaced > 0 )
{ {
File.WriteAllBytes( file.FullName, text ); WriteBackup( file.FullName, data );
File.WriteAllBytes( file.FullName, mdlFile.Write() );
} }
return replaced; return replaced;
@ -154,11 +69,11 @@ public static class ModelChanger
public static bool ChangeModMaterials( ModData mod, string from, string to ) 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 return mod.Resources.ModFiles
.Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) .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." ); PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." );