Editor/Conversion/Source1/VtxReader.cs

Reader for Source engine VTX files used by the editor conversion pipeline. It parses VTX version 7 binary data into a hierarchy of VtxFile -> BodyParts -> Models -> Lods -> Meshes, and fills mesh index lists by reading strip-group index buffers and mapping them to model vertex indices.

File Access
using System;
using System.Collections.Generic;
using System.IO;

namespace GmodAssetImporter;

public sealed class VtxFile
{
	public List<VtxBodyPart> BodyParts { get; } = new();
}

public sealed class VtxBodyPart
{
	public List<VtxModel> Models { get; } = new();
}

public sealed class VtxModel
{
	public List<VtxLod> Lods { get; } = new();
}

public sealed class VtxLod
{
	public List<VtxMesh> Meshes { get; } = new();
}

public sealed class VtxMesh
{
	public List<int> Indices { get; } = new(); // triangle list indices into model vertex range
}

/// <summary>
/// VTX reader — uses the strip-group index buffer as a triangle list (same as the first
/// working import). Fancy strip expansion proved less reliable on INS2 weapon VTXs.
/// </summary>
public static class VtxReader
{
	public static string FindVtxPath( string mdlPath )
	{
		var dir = Path.GetDirectoryName( mdlPath )!;
		var name = Path.GetFileNameWithoutExtension( mdlPath );
		var candidates = new[]
		{
			Path.Combine( dir, name + ".dx90.vtx" ),
			Path.Combine( dir, name + ".dx80.vtx" ),
			Path.Combine( dir, name + ".sw.vtx" ),
			Path.Combine( dir, name + ".vtx" )
		};
		return PathResolve.FindExistingFile( candidates );
	}

	public static VtxFile Load( string path ) => Load( File.ReadAllBytes( path ) );

	public static VtxFile Load( byte[] data )
	{
		using var ms = new MemoryStream( data );
		using var br = new BinaryReader( ms );

		var version = br.ReadInt32();
		if ( version != 7 )
			throw new InvalidDataException( $"Unsupported VTX version {version}" );

		br.ReadInt32(); // vertCacheSize
		br.ReadUInt16(); // maxBonesPerStrip
		br.ReadUInt16(); // maxBonesPerTri
		br.ReadInt32(); // maxBonesPerVert
		br.ReadInt32(); // checkSum
		br.ReadInt32(); // numLODs
		var materialReplacementListOffset = br.ReadInt32();
		var numBodyParts = br.ReadInt32();
		var bodyPartOffset = br.ReadInt32();
		_ = materialReplacementListOffset;

		var file = new VtxFile();
		for ( var bp = 0; bp < numBodyParts; bp++ )
		{
			var bpPos = bodyPartOffset + bp * 8;
			br.BaseStream.Position = bpPos;
			var numModels = br.ReadInt32();
			var modelOffset = br.ReadInt32();

			var body = new VtxBodyPart();
			for ( var m = 0; m < numModels; m++ )
			{
				var modelPos = bpPos + modelOffset + m * 8;
				br.BaseStream.Position = modelPos;
				var numLods = br.ReadInt32();
				var lodOffset = br.ReadInt32();

				var model = new VtxModel();
				if ( numLods > 0 )
				{
					var lodPos = modelPos + lodOffset;
					br.BaseStream.Position = lodPos;
					var numMeshes = br.ReadInt32();
					var meshOffset = br.ReadInt32();
					br.ReadSingle(); // switchPoint

					var lod = new VtxLod();
					for ( var me = 0; me < numMeshes; me++ )
					{
						var meshPos = lodPos + meshOffset + me * 9;
						br.BaseStream.Position = meshPos;
						var numStripGroups = br.ReadInt32();
						var stripGroupOffset = br.ReadInt32();
						br.ReadByte();

						var mesh = new VtxMesh();
						for ( var sg = 0; sg < numStripGroups; sg++ )
						{
							var sgPos = meshPos + stripGroupOffset + sg * 25;
							br.BaseStream.Position = sgPos;
							var numVerts = br.ReadInt32();
							var vertOffset = br.ReadInt32();
							var numIndices = br.ReadInt32();
							var indexOffset = br.ReadInt32();
							br.ReadInt32(); // numStrips
							br.ReadInt32(); // stripOffset
							br.ReadByte(); // flags

							var vertMap = new int[numVerts];
							br.BaseStream.Position = sgPos + vertOffset;
							for ( var v = 0; v < numVerts; v++ )
							{
								br.ReadByte(); br.ReadByte(); br.ReadByte();
								br.ReadByte();
								vertMap[v] = br.ReadUInt16();
								br.ReadByte(); br.ReadByte(); br.ReadByte();
							}

							br.BaseStream.Position = sgPos + indexOffset;
							for ( var i = 0; i < numIndices; i++ )
							{
								var idx = br.ReadUInt16();
								if ( idx < vertMap.Length )
									mesh.Indices.Add( vertMap[idx] );
							}
						}

						lod.Meshes.Add( mesh );
					}

					model.Lods.Add( lod );
				}

				body.Models.Add( model );
			}

			file.BodyParts.Add( body );
		}

		return file;
	}
}