Editor/Tools/WorkflowTools.AssetDependencies.cs

Editor-side tool that scans a given project asset file (prefab, scene, model, material, sound, etc.) and builds a dependency list by regex-parsing referenced asset and package identifiers, checking whether referenced files exist on disk and categorizing them.

File AccessReflection
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		private static readonly Regex AssetRefRegex = new(
			@"(?:""|')([^""']+\.(?:vmdl|vmat|vtex|sound|prefab|scene|vanim|shader|png|jpg|fbx|glb|mp3|wav|ogg))(?:""|')",
			RegexOptions.Compiled | RegexOptions.IgnoreCase );

		private static readonly Regex PackageRefRegex = new(
			@"(?:package:|ident:)([a-zA-Z0-9_\-\.]+\.[a-zA-Z0-9_\-\.]+)",
			RegexOptions.Compiled | RegexOptions.IgnoreCase );

		/// <summary>
		/// Scans a project asset (.prefab, .scene, .vmdl, .vmat, .sound, .vanim) and builds a full dependency tree
		/// of referenced materials, textures, sounds, models, and child prefabs, verifying integrity on disk.
		/// </summary>
		[McpTool( "pocketknife_asset_dependencies" )]
		public static string AssetDependencies(
			[Description( "Relative path to asset inside project (e.g. 'scenes/untitled.scene', 'prefabs/player.prefab', 'sounds/Camera.sound')" )] string assetPath )
		{
			try
			{
				var root = Project.Current?.GetRootPath();
				if ( string.IsNullOrWhiteSpace( root ) )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "project.not_active", "No active project is loaded." ) );
				}

				if ( string.IsNullOrWhiteSpace( assetPath ) )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "asset.path_required", "assetPath is required." ) );
				}

				var normalized = assetPath.Replace( '\\', '/' ).TrimStart( '/' );
				var fullPath = Path.IsPathRooted( normalized ) ? normalized : Path.Combine( root, normalized );

				if ( !File.Exists( fullPath ) )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( new { Path = normalized, FullPath = fullPath }, ToolStatus.Unavailable, "asset.not_found", $"Asset file '{normalized}' not found on disk." ) );
				}

				var content = File.ReadAllText( fullPath );
				var foundRefs = new HashSet<string>( StringComparer.OrdinalIgnoreCase );

				foreach ( Match m in AssetRefRegex.Matches( content ) )
				{
					var refPath = m.Groups[1].Value.Trim().Replace( '\\', '/' );
					if ( !refPath.Equals( normalized, StringComparison.OrdinalIgnoreCase ) )
					{
						foundRefs.Add( refPath );
					}
				}

				var packageRefs = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
				foreach ( Match m in PackageRefRegex.Matches( content ) )
				{
					packageRefs.Add( m.Groups[1].Value.Trim() );
				}

				var resolvedDeps = new List<object>();
				var missingDeps = new List<string>();

				foreach ( var r in foundRefs )
				{
					var ext = Path.GetExtension( r ).ToLowerInvariant();
					var category = ext switch
					{
						".vmdl" or ".fbx" or ".glb" => "Model",
						".vmat" => "Material",
						".vtex" or ".png" or ".jpg" => "Texture",
						".sound" or ".wav" or ".mp3" or ".ogg" => "Sound",
						".prefab" => "Prefab",
						".scene" => "Scene",
						".vanim" => "Animation",
						".shader" => "Shader",
						_ => "Asset"
					};

					var localDepPath = Path.Combine( root, r.TrimStart( '/' ) );
					var existsLocally = File.Exists( localDepPath );
					var status = existsLocally ? "FoundLocally" : (r.StartsWith( "models/" ) || r.StartsWith( "materials/" ) || r.StartsWith( "sounds/" ) ? "EngineOrCloudAsset" : "Missing");

					if ( status == "Missing" )
					{
						missingDeps.Add( r );
					}

					resolvedDeps.Add( new
					{
						Reference = r,
						Category = category,
						Extension = ext,
						Status = status,
						ExistsOnDisk = existsLocally
					} );
				}

				return McpJson.Write( ToolOutcome<object>.Ready( new
				{
					TargetAsset = normalized,
					FileSize = new FileInfo( fullPath ).Length,
					TotalDependencies = resolvedDeps.Count,
					PackageDependencies = packageRefs.ToArray(),
					MissingCount = missingDeps.Count,
					MissingDependencies = missingDeps,
					Dependencies = resolvedDeps,
					Success = true
				} ) );
			}
			catch ( Exception ex )
			{
				return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "asset.dependency_scan_failed", "Asset dependency analysis failed.", ex ) );
			}
		}
	}
}