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