Editor/Import/SceneDebugTools.cs

Editor toolset class providing debug and test utilities for the Unreal importer. It exposes Mcp tools that run imports from a staging folder, read mesh stats from .uasset files, run a headless export while collecting progress, open the importer window, and report model bounds for individual or batch VMDL files.

File AccessProcess ExecutionNetworking
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Editor.Mcp;
using Sandbox;

namespace Editor.UnrealImporter;

// TEMPORARY verification tool - delete once scene import scale is confirmed.
[McpToolset( "unrealimporter", "Unreal importer debug tools" )]
public static class SceneDebugTools
{
	/// <summary>Run AssetImporter.Import on a staging folder (manifest.json + FBX + PNG).</summary>
	/// <param name="stagingDir">Staging folder containing manifest.json.</param>
	/// <param name="outputFolder">Output folder inside the project's Assets/.</param>
	/// <param name="layout">Grouped (default), Flat, ClassicSource or PerAsset.</param>
	/// <param name="materialOutput">Material (default), Terrain or Decal.</param>
	/// <param name="perAssetFolderDepth">PerAsset layout: folders up the /Game path to name each folder after (0 = own name).</param>
	/// <param name="maxTextureSize">Cap every written texture's longest edge, downscaling bigger sources (0 = keep as-is).</param>
	[McpTool( "unreal_scene_import_test" )]
	public static async Task<string> SceneImportTest( string stagingDir, string outputFolder, string layout = null, string materialOutput = null, int perAssetFolderDepth = 0, int maxTextureSize = 0 )
	{
		var manifestPath = Path.Combine( stagingDir, "manifest.json" );
		if ( !File.Exists( manifestPath ) )
			return $"no manifest.json in {stagingDir}";

		if ( !System.Enum.TryParse<ImportLayout>( layout ?? "Grouped", ignoreCase: true, out var importLayout ) )
			return $"unknown layout '{layout}'";
		if ( !System.Enum.TryParse<MaterialOutput>( materialOutput ?? "Material", ignoreCase: true, out var matOutput ) )
			return $"unknown material output '{materialOutput}'";

		var manifest = ImportManifest.Load( manifestPath );
		var summary = await AssetImporter.Import( manifest, stagingDir, outputFolder, CancellationToken.None, importLayout, materialOutput: matOutput, perAssetFolderDepth: perAssetFolderDepth, maxTextureSize: maxTextureSize );

		var result = $"models={summary.Models} materials={summary.Materials} textures={summary.Textures} " +
			$"placements={summary.Placements} prefab={summary.PrefabPath ?? "(none)"}";
		if ( summary.Warnings.Count > 0 )
			result += "\nwarnings:\n - " + string.Join( "\n - ", summary.Warnings.Take( 10 ) );

		return result;
	}

	/// <summary>TEMP: read tri/vert counts from a .uasset via UassetMeshStats.</summary>
	/// <param name="uassetPath">Absolute path to a .uasset.</param>
	[McpTool( "unreal_meshstats_test" )]
	public static async Task<string> MeshStatsTest( string uassetPath )
	{
		var stats = await UassetMeshStats.LoadAsync( uassetPath );
		if ( stats is null )
			return "no stats found";

		return $"tris={stats.Triangles} verts={stats.Vertices} mats={stats.Materials} lods={stats.LODs}";
	}

	/// <summary>TEMP: run a small headless export and log the live progress events (verifies log tailing).</summary>
	/// <param name="uprojectPath">Absolute path to the .uproject.</param>
	/// <param name="assets">';'-separated /Game asset paths to export.</param>
	[McpTool( "unreal_export_progress_test" )]
	public static async Task<string> ExportProgressTest( string uprojectPath, string assets )
	{
		var script = HeadlessExporter.FindExportScript();
		var editorCmd = UnrealLocator.FindEditorCmd( UnrealLocator.ReadEngineAssociation( uprojectPath ) );
		if ( script is null || editorCmd is null )
			return "tools not found";

		var events = new List<string>();
		var result = await HeadlessExporter.Run( editorCmd, uprojectPath, assets.Split( ';' ), script, System.Threading.CancellationToken.None,
			onProgress: ev =>
			{
				var line = $"{ev.Done}/{ev.Total} {ev.Message}";
				events.Add( line );
				Log.Info( $"UEPROG {line}" );
			} );

		return $"success={result.Success} events={events.Count}\n" + string.Join( "\n", events.TakeLast( 12 ) );
	}

	/// <summary>TEMP: open the Unreal Importer window for UI verification.</summary>
	[McpTool( "unreal_open_import_window" )]
	public static string OpenImportWindow()
	{
		_ = new UnrealImportWindow();
		return "opened";
	}

	/// <summary>Load a model and report its bounds in inches.</summary>
	/// <param name="modelPath">Model content path, e.g. "unrealimport/models/x.vmdl".</param>
	[McpTool( "unreal_model_bounds" )]
	public static string ModelBounds( string modelPath )
	{
		var model = Model.Load( modelPath );
		if ( model is null || model.IsError )
			return $"failed to load {modelPath}";

		var b = model.Bounds;
		return $"size=({b.Size.x:0.##}, {b.Size.y:0.##}, {b.Size.z:0.##}) in  mins=({b.Mins.x:0.##},{b.Mins.y:0.##},{b.Mins.z:0.##}) maxs=({b.Maxs.x:0.##},{b.Maxs.y:0.##},{b.Maxs.z:0.##})";
	}

	/// <summary>Bounds of every .vmdl in a folder, one json object per line.</summary>
	/// <param name="folder">Absolute folder containing .vmdl files.</param>
	[McpTool( "unreal_all_model_bounds" )]
	public static string AllModelBounds( string folder )
	{
		var sb = new System.Text.StringBuilder();
		foreach ( var f in Directory.EnumerateFiles( folder, "*.vmdl" ) )
		{
			var rel = Path.GetRelativePath( Sandbox.Project.Current.GetAssetsPath(), f ).Replace( '\\', '/' );
			var model = Model.Load( rel );
			if ( model is null || model.IsError )
			{
				sb.AppendLine( $"{{\"model\":\"{Path.GetFileName( f )}\",\"error\":true}}" );
				continue;
			}

			var b = model.Bounds;
			sb.AppendLine( System.FormattableString.Invariant(
				$"{{\"model\":\"{Path.GetFileName( f )}\",\"min\":[{b.Mins.x:0.###},{b.Mins.y:0.###},{b.Mins.z:0.###}],\"max\":[{b.Maxs.x:0.###},{b.Maxs.y:0.###},{b.Maxs.z:0.###}]}}" ) );
		}
		return sb.ToString();
	}
}