Editor/Tools/WorkflowTools.SceneBakeAndHammer.cs

Editor tools for scene operations and Hammer integration. Provides Mcp tools to bake lighting/envmaps/navmesh, save the active scene headlessly to disk, and inspect/resolve Hammer editor sessions via reflection.

File AccessReflection
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Sandbox;
using Sandbox.Navigation;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		/// <summary>
		/// Triggers engine baking operations: light probes (IndirectLightVolume), envmap reflection probes,
		/// navigation mesh (NavMesh), or compiles scene assets.
		/// </summary>
		[McpTool( "pocketknife_scene_bake" )]
		public static async Task<string> SceneBake(
			[Description( "Bake target: 'light_probes', 'envmaps', 'navmesh', 'all'" )] string target = "all",
			[Description( "Save scene after baking is completed (only applies if scene has an established file path)" )] bool saveAfter = false )
		{
			try
			{
				var scene = EditorRuntime.ResolveActiveScene();
				if ( scene == null )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "scene.not_found", "No active scene found." ) );
				}

				var normTarget = target.ToLowerInvariant().Trim();
				var bakedItems = new List<string>();

				if ( normTarget == "all" || normTarget == "light_probes" || normTarget == "lighting" )
				{
					await IndirectLightVolume.BakeAll();
					bakedItems.Add( "IndirectLightVolume (light probes)" );
				}

				if ( normTarget == "all" || normTarget == "envmaps" || normTarget == "reflections" )
				{
					await EnvmapProbe.BakeAll();
					bakedItems.Add( "EnvmapProbe (reflection probes)" );
				}

				if ( normTarget == "all" || normTarget == "navmesh" || normTarget == "navigation" )
				{
					NavMesh.BakeNavMesh();
					bakedItems.Add( "NavMesh (pathfinding grid)" );
				}

				if ( saveAfter && !string.IsNullOrEmpty( scene.Source?.ResourcePath ) )
				{
					EditorRuntime.InvokeStatic( "Editor.Mcp.SceneTools", "SaveScene" );
				}

				return McpJson.Write( ToolOutcome<object>.Ready( new
				{
					Target = normTarget,
					BakedComponents = bakedItems,
					SceneSaved = saveAfter,
					Success = true
				} ) );
			}
			catch ( Exception ex )
			{
				return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "bake.failed", "Scene bake operation failed.", ex ) );
			}
		}

		/// <summary>
		/// Headlessly serializes and saves the active scene directly to disk without opening any OS file dialogs or modal prompts.
		/// </summary>
		[McpTool( "pocketknife_scene_save" )]
		public static string SceneSave(
			[Description( "Optional relative scene file path (e.g. 'scenes/untitled.scene' or 'scenes/main.scene'). If null, saves to scene's current file path or defaults to 'scenes/untitled.scene'." )] string? filePath = null )
		{
			try
			{
				var scene = EditorRuntime.ResolveActiveScene();
				if ( scene == null )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "scene.not_found", "No active scene found to save." ) );
				}

				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 available." ) );
				}

				var targetPath = filePath;
				if ( string.IsNullOrWhiteSpace( targetPath ) )
				{
					targetPath = scene.Source?.ResourcePath;
				}
				if ( string.IsNullOrWhiteSpace( targetPath ) )
				{
					targetPath = "scenes/untitled.scene";
				}

				var fullPath = Path.IsPathRooted( targetPath ) ? targetPath : Path.Combine( root, targetPath );
				Directory.CreateDirectory( Path.GetDirectoryName( fullPath )! );

				var jsonNode = scene.Serialize();
				var jsonText = jsonNode?.ToJsonString() ?? "{}";
				File.WriteAllText( fullPath, jsonText );

				return McpJson.Write( ToolOutcome<object>.Ready( new
				{
					Path = targetPath,
					FullPath = fullPath,
					Bytes = jsonText.Length,
					Success = true,
					SavedHeadlessly = true
				} ) );
			}
			catch ( Exception ex )
			{
				return McpJson.Write( ToolOutcome<object>.Failure( new { Error = ex.ToString() }, ToolStatus.Failed, "scene.save_failed", $"Headless scene save failed: {ex.Message}", ex ) );
			}
		}

		/// <summary>
		/// Inspects or interacts with Hammer map editor sessions: queries open .vmap documents, map world objects,
		/// unsaved changes, selections, and can focus or resolve map sessions.
		/// </summary>
		[McpTool( "pocketknife_hammer_session" )]
		public static string HammerSession(
			[Description( "Action: 'list_sessions', 'inspect', 'focus', 'resolve'" )] string action = "list_sessions",
			[Description( "Path or name of the .vmap file when action is 'resolve' or 'inspect'" )] string? mapPath = null )
		{
			try
			{
				var hammerSessionType = EditorRuntime.FindType( "Editor.MapEditor.HammerSceneEditorSession" );
				if ( hammerSessionType == null )
				{
					return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "hammer.not_available", "Hammer editor session type is not loaded." ) );
				}

				var normAction = action.ToLowerInvariant().Trim();

				if ( normAction == "list_sessions" )
				{
					var allProp = hammerSessionType.GetProperty( "All", BindingFlags.Public | BindingFlags.Static );
					var allSessions = allProp?.GetValue( null ) as System.Collections.IEnumerable;
					var sessionList = new List<object>();

					if ( allSessions != null )
					{
						foreach ( var s in allSessions )
						{
							var scene = EditorRuntime.GetMember( s, "Scene" ) as Scene;
							var hasChanges = EditorRuntime.GetMember( s, "HasUnsavedChanges" ) as bool? ?? false;
							sessionList.Add( new
							{
								SceneName = scene?.Name,
								HasUnsavedChanges = hasChanges,
								MapWorld = EditorRuntime.GetMember( s, "MapWorld" )?.ToString()
							} );
						}
					}

					return McpJson.Write( ToolOutcome<object>.Ready( new
					{
						TotalSessions = sessionList.Count,
						Sessions = sessionList
					} ) );
				}

				if ( normAction == "resolve" && !string.IsNullOrWhiteSpace( mapPath ) )
				{
					var resolveMethod = hammerSessionType.GetMethod( "Resolve", new[] { typeof( string ) } );
					var resolved = resolveMethod?.Invoke( null, new object[] { mapPath.Trim() } );
					if ( resolved != null )
					{
						var focusMethod = hammerSessionType.GetMethod( "Focus", Type.EmptyTypes );
						focusMethod?.Invoke( resolved, null );
						return McpJson.Write( ToolOutcome<object>.Ready( new
						{
							Action = "resolve",
							MapPath = mapPath,
							Focused = true
						} ) );
					}
					return McpJson.Write( ToolOutcome<object>.Failure( new { MapPath = mapPath }, ToolStatus.Failed, "hammer.resolve_failed", $"Failed to resolve Hammer map at '{mapPath}'." ) );
				}

				return McpJson.Write( ToolOutcome<object>.Ready( new { Action = normAction, Status = "Ready" } ) );
			}
			catch ( Exception ex )
			{
				return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "hammer.failed", "Hammer session interaction failed.", ex ) );
			}
		}
	}
}