Editor/UI/MaterialAssets.Sidecars.cs
// Copied from humanoid-retargeter Editor/HumanoidRetargeter/EditorPipeline.cs.
using Editor;
using Sandbox;
namespace HumanoidRigger.Editor;
internal static partial class MaterialAssets
{
	static readonly string[] TextureExtensions =
		{ ".png", ".jpg", ".jpeg", ".tga", ".dds", ".webp", ".vmat", ".vtex" };

	/// <summary>Copies texture sidecars of a picked target model into the output folder:
	/// loose image files next to it, and a "textures" folder next to it or next to its
	/// parent (the source/-plus-textures/ layout). Per-file best effort - a failed texture
	/// must never fail the conversion.</summary>
	static void CopySidecarTextures( string sourceDir, string destDir )
	{
		try
		{
			if ( sourceDir is null || destDir is null )
				return;
			sourceDir = Path.GetFullPath( sourceDir );
			destDir = Path.GetFullPath( destDir );
			if ( string.Equals( sourceDir, destDir, StringComparison.OrdinalIgnoreCase ) )
				return;

			// Every copied file must be REGISTERED: assets copied onto disk mid-session are
			// unknown to the asset system, so the material chain cannot generate their vtex
			// resources - the renderer then logs "Texture manager doesn't know about
			// texture ...generated.vtex" MANY TIMES PER FRAME, which is both the
			// purple/black flicker and a preview running at ~2 fps (user report).
			foreach ( var file in Directory.GetFiles( sourceDir ) )
			{
				if ( !TextureExtensions.Contains( Path.GetExtension( file ).ToLowerInvariant() ) )
					continue;
				var destFile = Path.Combine( destDir, Path.GetFileName( file ) );
				Try( () => { File.Copy( file, destFile, true ); return true; } );
				Try( () => AssetSystem.RegisterFile( destFile ) );
			}

			foreach ( var candidate in new[]
			{
				Path.Combine( sourceDir, "textures" ),
				Path.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, "textures" ),
			} )
			{
				if ( !Directory.Exists( candidate ) )
					continue;
				var destTextures = Path.Combine( destDir, "textures" );
				Directory.CreateDirectory( destTextures );
				foreach ( var file in Directory.GetFiles( candidate, "*", SearchOption.AllDirectories ) )
				{
					var relative = Path.GetRelativePath( candidate, file );
					var destFile = Path.Combine( destTextures, relative );
					Try( () =>
					{
						Directory.CreateDirectory( Path.GetDirectoryName( destFile ) );
						File.Copy( file, destFile, true );
						return true;
					} );
					Try( () => AssetSystem.RegisterFile( destFile ) );
				}
				break; // first existing candidate wins
			}
		}
		catch ( Exception e )
		{
			Log.Warning( $"[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}" );
		}
	}


static T Try<T>(Func<T> action){try{return action();}catch(Exception e){Log.Warning(e.Message);return default;}}
}