Editor/SeamlessMaterialExporter.cs
using System;
using System.IO;
using System.Text;
using Editor;
using Sandbox;

public static class SeamlessMaterialExporter
{
	public static SeamlessMaterialExportResult Export( string texturePath, bool overwriteExisting )
	{
		var result = new SeamlessMaterialExportResult();

		try
		{
			if ( string.IsNullOrWhiteSpace( texturePath ) || !File.Exists( texturePath ) )
			{
				result.Error = "Texture file was not found.";
				return result;
			}

			var vmatPath = BuildVmatPath( texturePath, overwriteExisting );
			var outputFolder = Path.GetDirectoryName( vmatPath );

			if ( !string.IsNullOrWhiteSpace( outputFolder ) )
				Directory.CreateDirectory( outputFolder );

			var textureReferencePath = GetTextureReferencePath( texturePath, result );
			var vmatContent = GenerateVmatContent( textureReferencePath );

			File.WriteAllText( vmatPath, vmatContent );

			try
			{
				var asset = AssetSystem.RegisterFile( vmatPath );

				if ( asset == null )
					AddWarning( result, "The .vmat was written, but S&box did not return a registered material asset." );
			}
			catch ( Exception ex )
			{
				AddWarning( result, $"The .vmat was written, but S&box asset registration skipped it: {ex.Message}" );
			}

			result.Success = true;
			result.VmatPath = vmatPath;
			result.TextureReferencePath = textureReferencePath;
		}
		catch ( Exception ex )
		{
			result.Error = ex.Message;
		}

		return result;
	}

	private static string BuildVmatPath( string texturePath, bool overwriteExisting )
	{
		var basePath = Path.ChangeExtension( texturePath, ".vmat" );

		if ( overwriteExisting || !File.Exists( basePath ) )
			return basePath;

		var folder = Path.GetDirectoryName( basePath ) ?? "";
		var fileName = Path.GetFileNameWithoutExtension( basePath );

		for ( var i = 1; i < 1000; i++ )
		{
			var numberedPath = Path.Combine( folder, $"{fileName}_{i:000}.vmat" );

			if ( !File.Exists( numberedPath ) )
				return numberedPath;
		}

		return basePath;
	}

	private static string GetTextureReferencePath( string texturePath, SeamlessMaterialExportResult result )
	{
		var fullTexturePath = Path.GetFullPath( texturePath );
		var normalizedTexturePath = NormalizePath( fullTexturePath );
		var projectAssetsPath = Sandbox.Project.Current?.GetAssetsPath();

		if ( !string.IsNullOrWhiteSpace( projectAssetsPath ) )
		{
			var normalizedAssetsPath = NormalizePath( Path.GetFullPath( projectAssetsPath ) );

			if ( !normalizedAssetsPath.EndsWith( "/" ) )
				normalizedAssetsPath += "/";

			if ( normalizedTexturePath.StartsWith( normalizedAssetsPath, StringComparison.OrdinalIgnoreCase ) )
				return normalizedTexturePath.Substring( normalizedAssetsPath.Length );

			AddWarning( result, "The exported texture and .vmat are outside the active project's Assets folder. S&box may not treat them as normal project assets; choose a folder under Assets for best editor use." );
		}
		else
		{
			AddWarning( result, "Could not find the active project's Assets folder. The .vmat will reference the texture by path, but S&box may not treat it as a normal project asset." );
		}

		return normalizedTexturePath;
	}

	private static string GenerateVmatContent( string textureReferencePath )
	{
		var texturePath = NormalizePath( textureReferencePath ).Replace( "\"", "" );
		var sb = new StringBuilder();

		sb.AppendLine( "// THIS FILE IS AUTO-GENERATED" );
		sb.AppendLine( "// Generated by Seam-Less Texture Suite" );
		sb.AppendLine();
		sb.AppendLine( "Layer0" );
		sb.AppendLine( "{" );
		sb.AppendLine( "\tshader \"complex.shader\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Ambient Occlusion ----" );
		sb.AppendLine( "\tg_flAmbientOcclusionDirectDiffuse \"0.000\"" );
		sb.AppendLine( "\tTextureAmbientOcclusion \"materials/default/default_ao.tga\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Color ----" );
		sb.AppendLine( "\tg_flModelTintAmount \"1.000\"" );
		sb.AppendLine( "\tg_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"" );
		sb.AppendLine( $"\tTextureColor \"{texturePath}\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Fade ----" );
		sb.AppendLine( "\tg_flFadeExponent \"1.000\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Fog ----" );
		sb.AppendLine( "\tg_bFogEnabled \"1\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Metalness ----" );
		sb.AppendLine( "\tg_flMetalness \"0.000\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Normal ----" );
		sb.AppendLine( "\tTextureNormal \"materials/default/default_normal.tga\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Roughness ----" );
		sb.AppendLine( "\tg_flRoughnessScaleFactor \"1.000\"" );
		sb.AppendLine( "\tTextureRoughness \"materials/default/default_rough.tga\"" );
		sb.AppendLine();
		sb.AppendLine( "\t//---- Texture Coordinates ----" );
		sb.AppendLine( "\tg_vTexCoordOffset \"[0.000 0.000]\"" );
		sb.AppendLine( "\tg_vTexCoordScale \"[1.000 1.000]\"" );
		sb.AppendLine( "\tg_vTexCoordScrollSpeed \"[0.000 0.000]\"" );
		sb.AppendLine( "}" );

		return sb.ToString();
	}

	private static void AddWarning( SeamlessMaterialExportResult result, string warning )
	{
		if ( string.IsNullOrWhiteSpace( warning ) )
			return;

		if ( string.IsNullOrWhiteSpace( result.Warning ) )
		{
			result.Warning = warning;
			return;
		}

		result.Warning += $" {warning}";
	}

	private static string NormalizePath( string path )
	{
		return path?.Replace( '\\', '/' ) ?? "";
	}
}

public class SeamlessMaterialExportResult
{
	public bool Success { get; set; }
	public string VmatPath { get; set; }
	public string TextureReferencePath { get; set; }
	public string Warning { get; set; }
	public string Error { get; set; }
}