using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Helpers; // For Response class
using MCPForUnity.Runtime.Helpers; // For ScreenshotUtility
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MCPForUnity.Editor.Tools
{
///
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
///
[McpForUnityTool("manage_scene", AutoRegister = false)]
public static class ManageScene
{
private sealed class SceneCommand
{
public string action { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public string path { get; set; } = string.Empty;
public int? buildIndex { get; set; }
public string fileName { get; set; } = string.Empty;
public int? superSize { get; set; }
// screenshot: camera selection, inline image, batch, view positioning
public string camera { get; set; }
public string captureSource { get; set; } // "game_view" (default) or "scene_view"
public bool? includeImage { get; set; }
public int? maxResolution { get; set; }
public string outputFolder { get; set; } // optional override; null falls back to user pref / Assets/Screenshots
public string batch { get; set; } // "surround" or "orbit" for multi-angle batch capture
public JToken viewTarget { get; set; } // GO reference or [x,y,z] to focus on before capture
public Vector3? viewPosition { get; set; } // camera position for view-based capture
public Vector3? viewRotation { get; set; } // euler rotation for view-based capture
// orbit batch params
public int? orbitAngles { get; set; } // number of azimuth samples (default 8)
public float[] orbitElevations { get; set; } // elevation angles in degrees (default [0, 30, -15])
public float? orbitDistance { get; set; } // camera distance from target (default auto from bounds)
public float? orbitFov { get; set; } // camera FOV in degrees (default 60)
// scene_view_frame
public JToken sceneViewTarget { get; set; }
// get_hierarchy paging + safety (summary-first)
public JToken parent { get; set; }
public int? pageSize { get; set; }
public int? cursor { get; set; }
public int? maxNodes { get; set; }
public int? maxDepth { get; set; }
public int? maxChildrenPerNode { get; set; }
public bool? includeTransform { get; set; }
// Multi-scene editing
public string sceneName { get; set; }
public string scenePath { get; set; }
public string target { get; set; } // GO reference for move_to_scene
public bool? removeScene { get; set; } // for close_scene
public bool? additive { get; set; } // for load additive mode
public string template { get; set; } // for create with template
public bool? autoRepair { get; set; } // for validate with auto-repair
}
private static float[] ParseFloatArray(JToken token)
{
if (token == null || token.Type == JTokenType.Null) return null;
if (token.Type == JTokenType.Array)
{
var arr = (JArray)token;
var result = new float[arr.Count];
for (int i = 0; i < arr.Count; i++)
{
try
{
result[i] = arr[i].ToObject();
}
catch (Exception ex)
{
throw new Newtonsoft.Json.JsonException(
$"Failed to parse float at index {i}: '{arr[i]}'", ex);
}
}
return result;
}
// Single value → array of one
var single = ParamCoercion.CoerceFloatNullable(token);
return single.HasValue ? new[] { single.Value } : null;
}
private static SceneCommand ToSceneCommand(JObject p)
{
if (p == null) return new SceneCommand();
var toolParams = new ToolParams(p);
return new SceneCommand
{
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
name = p["name"]?.ToString() ?? string.Empty,
path = p["path"]?.ToString() ?? string.Empty,
buildIndex = ParamCoercion.CoerceIntNullable(p["buildIndex"] ?? p["build_index"]),
fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty,
superSize = ParamCoercion.CoerceIntNullable(p["superSize"] ?? p["super_size"] ?? p["supersize"]),
// screenshot: camera selection, inline image, batch, view positioning
camera = (p["camera"])?.ToString(),
captureSource = toolParams.Get("capture_source"),
includeImage = ParamCoercion.CoerceBoolNullable(p["includeImage"] ?? p["include_image"]),
maxResolution = ParamCoercion.CoerceIntNullable(p["maxResolution"] ?? p["max_resolution"]),
outputFolder = (p["outputFolder"] ?? p["output_folder"])?.ToString(),
batch = (p["batch"])?.ToString(),
viewTarget = p["viewTarget"] ?? p["view_target"],
viewPosition = VectorParsing.ParseVector3(p["viewPosition"] ?? p["view_position"]),
viewRotation = VectorParsing.ParseVector3(p["viewRotation"] ?? p["view_rotation"]),
// orbit batch params
orbitAngles = ParamCoercion.CoerceIntNullable(p["orbitAngles"] ?? p["orbit_angles"]),
orbitElevations = ParseFloatArray(p["orbitElevations"] ?? p["orbit_elevations"]),
orbitDistance = ParamCoercion.CoerceFloatNullable(p["orbitDistance"] ?? p["orbit_distance"]),
orbitFov = ParamCoercion.CoerceFloatNullable(p["orbitFov"] ?? p["orbit_fov"]),
// scene_view_frame
sceneViewTarget = toolParams.GetRaw("scene_view_target"),
// get_hierarchy paging + safety
parent = p["parent"],
pageSize = ParamCoercion.CoerceIntNullable(p["pageSize"] ?? p["page_size"]),
cursor = ParamCoercion.CoerceIntNullable(p["cursor"]),
maxNodes = ParamCoercion.CoerceIntNullable(p["maxNodes"] ?? p["max_nodes"]),
maxDepth = ParamCoercion.CoerceIntNullable(p["maxDepth"] ?? p["max_depth"]),
maxChildrenPerNode = ParamCoercion.CoerceIntNullable(p["maxChildrenPerNode"] ?? p["max_children_per_node"]),
includeTransform = ParamCoercion.CoerceBoolNullable(p["includeTransform"] ?? p["include_transform"]),
// Multi-scene editing
sceneName = (p["sceneName"] ?? p["scene_name"])?.ToString(),
scenePath = (p["scenePath"] ?? p["scene_path"])?.ToString(),
target = (p["target"])?.ToString(),
removeScene = ParamCoercion.CoerceBoolNullable(p["removeScene"] ?? p["remove_scene"]),
additive = ParamCoercion.CoerceBoolNullable(p["additive"]),
template = (p["template"])?.ToString()?.ToLowerInvariant(),
autoRepair = ParamCoercion.CoerceBoolNullable(p["autoRepair"] ?? p["auto_repair"]),
};
}
private static Scene? FindLoadedScene(string sceneName, string scenePath)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!string.IsNullOrEmpty(scenePath) && scene.path == scenePath)
return scene;
if (!string.IsNullOrEmpty(sceneName) && scene.name == sceneName)
return scene;
}
return null;
}
///
/// Main handler for scene management actions.
///
public static object HandleCommand(JObject @params)
{
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
var cmd = ToSceneCommand(@params);
string action = cmd.action;
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
int? buildIndex = cmd.buildIndex;
// bool loadAdditive = @params["loadAdditive"]?.ToObject() ?? false; // Example for future extension
// Ensure path is relative to Assets/, removing any leading "Assets/"
string relativeDir = path ?? string.Empty;
if (!string.IsNullOrEmpty(relativeDir))
{
relativeDir = AssetPathUtility.NormalizeSeparators(relativeDir).Trim('/');
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
}
// If path ends with .unity, it's a full scene path — extract just the directory
if (relativeDir.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
{
string dirPart = Path.GetDirectoryName(relativeDir);
relativeDir = string.IsNullOrEmpty(dirPart)
? string.Empty
: AssetPathUtility.NormalizeSeparators(dirPart);
}
}
// Apply default *after* sanitizing, using the original path variable for the check
if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness
{
relativeDir = "Scenes"; // Default relative directory
}
if (string.IsNullOrEmpty(action))
{
return new ErrorResponse("Action parameter is required.");
}
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
string fullPath = string.IsNullOrEmpty(sceneFileName)
? null
: Path.Combine(fullPathDir, sceneFileName);
// Ensure relativePath always starts with "Assets/" and uses forward slashes
string relativePath = string.IsNullOrEmpty(sceneFileName)
? null
: AssetPathUtility.NormalizeSeparators(Path.Combine("Assets", relativeDir, sceneFileName));
// Ensure directory exists for 'create'
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
{
try
{
Directory.CreateDirectory(fullPathDir);
}
catch (Exception e)
{
return new ErrorResponse(
$"Could not create directory '{fullPathDir}': {e.Message}"
);
}
}
// Route action
try { McpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { }
switch (action)
{
case "create":
if (string.IsNullOrEmpty(name))
return new ErrorResponse(
"'name' parameter is required for 'create' action. 'path' is optional (defaults to 'Assets/Scenes/')."
);
if (!string.IsNullOrEmpty(cmd.template))
return CreateSceneFromTemplate(fullPath, relativePath, cmd.template);
return CreateScene(fullPath, relativePath);
case "load":
// Loading can be done by path/name or build index
// When path ends with .unity and no name is given, use path directly as the scene path
string loadPath = relativePath;
if (string.IsNullOrEmpty(loadPath) && !string.IsNullOrEmpty(path))
loadPath = AssetPathUtility.NormalizeSeparators(
path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)
? path : "Assets/" + path);
if (!string.IsNullOrEmpty(loadPath))
{
if (cmd.additive == true)
return LoadSceneAdditive(loadPath);
return LoadScene(loadPath);
}
else if (buildIndex.HasValue)
return LoadScene(buildIndex.Value);
else
return new ErrorResponse(
"Either 'name'/'path' or 'buildIndex' must be provided for 'load' action."
);
case "save":
// Save current scene, optionally to a new path
return SaveScene(fullPath, relativePath);
case "get_hierarchy":
try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
var gh = GetSceneHierarchyPaged(cmd);
try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
return gh;
case "get_active":
try { McpLog.Info("[ManageScene] get_active: entering", always: false); } catch { }
var ga = GetActiveSceneInfo();
try { McpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { }
return ga;
case "get_build_settings":
return GetBuildSettingsScenes();
case "screenshot":
return CaptureScreenshot(cmd);
case "scene_view_frame":
return FrameSceneView(cmd);
// Multi-scene editing
case "close_scene":
return CloseScene(cmd);
case "set_active_scene":
return SetActiveScene(cmd);
case "get_loaded_scenes":
return GetLoadedScenes();
case "move_to_scene":
return MoveToScene(cmd);
case "modify_build_settings":
return new ErrorResponse(
"Build settings management has moved to manage_build (action='scenes'). "
+ "Use manage_build to add, remove, or configure scenes in build settings.");
// Scene validation
case "validate":
return ValidateScene(cmd.autoRepair == true);
default:
return new ErrorResponse(
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings, screenshot, scene_view_frame, close_scene, set_active_scene, get_loaded_scenes, move_to_scene, validate. For build settings, use manage_build."
);
}
}
///
/// Captures a screenshot to Assets/Screenshots and returns a response payload.
/// Public so the tools UI can reuse the same logic without duplicating parameters.
/// Available in both Edit Mode and Play Mode.
///
public static object ExecuteScreenshot(string fileName = null, int? superSize = null)
{
var cmd = new SceneCommand { fileName = fileName ?? string.Empty, superSize = superSize };
return CaptureScreenshot(cmd);
}
///
/// Captures a 6-angle contact-sheet around the scene bounds centre.
/// Public so the tools UI can reuse the same logic.
///
///
/// Captures the active Scene View viewport to a PNG asset.
/// Public so the tools UI can reuse the same logic.
///
public static object ExecuteSceneViewScreenshot(string fileName = null)
{
var cmd = new SceneCommand { fileName = fileName ?? string.Empty };
return CaptureSceneViewScreenshot(cmd, cmd.fileName, 1, false, 0);
}
public static object ExecuteMultiviewScreenshot(int maxResolution = 480)
{
var cmd = new SceneCommand { maxResolution = maxResolution };
return CaptureSurroundBatch(cmd);
}
private static object CreateScene(string fullPath, string relativePath)
{
if (File.Exists(fullPath))
{
return new ErrorResponse($"Scene already exists at '{relativePath}'.");
}
try
{
// Create a new empty scene
Scene newScene = EditorSceneManager.NewScene(
NewSceneSetup.EmptyScene,
NewSceneMode.Single
);
// Save it to the specified path
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
if (saved)
{
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity sees the new scene file
return new SuccessResponse(
$"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.",
new { path = relativePath }
);
}
else
{
// If SaveScene fails, it might leave an untitled scene open.
// Optionally try to close it, but be cautious.
return new ErrorResponse($"Failed to save new scene to '{relativePath}'.");
}
}
catch (Exception e)
{
return new ErrorResponse($"Error creating scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(string relativePath)
{
if (
!File.Exists(
Path.Combine(
Application.dataPath.Substring(
0,
Application.dataPath.Length - "Assets".Length
),
relativePath
)
)
)
{
return new ErrorResponse($"Scene file not found at '{relativePath}'.");
}
// Check for unsaved changes in the current scene
if (EditorSceneManager.GetActiveScene().isDirty)
{
// Optionally prompt the user or save automatically before loading
return new ErrorResponse(
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
);
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
// if (!saveOK) return new ErrorResponse("Load cancelled by user.");
}
try
{
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
return new SuccessResponse(
$"Scene '{relativePath}' loaded successfully.",
new
{
path = relativePath,
name = Path.GetFileNameWithoutExtension(relativePath),
}
);
}
catch (Exception e)
{
return new ErrorResponse($"Error loading scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(int buildIndex)
{
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
{
return new ErrorResponse(
$"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}."
);
}
// Check for unsaved changes
if (EditorSceneManager.GetActiveScene().isDirty)
{
return new ErrorResponse(
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
);
}
try
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
return new SuccessResponse(
$"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.",
new
{
path = scenePath,
name = Path.GetFileNameWithoutExtension(scenePath),
buildIndex = buildIndex,
}
);
}
catch (Exception e)
{
return new ErrorResponse(
$"Error loading scene with build index {buildIndex}: {e.Message}"
);
}
}
private static object SaveScene(string fullPath, string relativePath)
{
try
{
Scene currentScene = EditorSceneManager.GetActiveScene();
if (!currentScene.IsValid())
{
return new ErrorResponse("No valid scene is currently active to save.");
}
bool saved;
string finalPath = currentScene.path; // Path where it was last saved or will be saved
if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath)
{
// Save As...
// Ensure directory exists
string dir = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
finalPath = relativePath;
}
else
{
// Save (overwrite existing or save untitled)
if (string.IsNullOrEmpty(currentScene.path))
{
// Scene is untitled, needs a path
return new ErrorResponse(
"Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality."
);
}
saved = EditorSceneManager.SaveScene(currentScene);
}
if (saved)
{
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
return new SuccessResponse(
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
new { path = finalPath, name = currentScene.name }
);
}
else
{
return new ErrorResponse($"Failed to save scene '{currentScene.name}'.");
}
}
catch (Exception e)
{
return new ErrorResponse($"Error saving scene: {e.Message}");
}
}
private static object CaptureScreenshot(SceneCommand cmd)
{
try
{
string fileName = cmd.fileName;
int resolvedSuperSize = (cmd.superSize.HasValue && cmd.superSize.Value > 0) ? cmd.superSize.Value : 1;
bool includeImage = cmd.includeImage ?? false;
int maxResolution = cmd.maxResolution ?? 0; // 0 = let ScreenshotUtility default to 640
string cameraRef = cmd.camera;
string captureSource = string.IsNullOrWhiteSpace(cmd.captureSource)
? "game_view"
: cmd.captureSource.Trim().ToLowerInvariant();
if (captureSource != "game_view" && captureSource != "scene_view")
{
return new ErrorResponse(
$"Invalid capture_source '{cmd.captureSource}'. Valid values: 'game_view', 'scene_view'.");
}
if (captureSource == "scene_view")
{
if (resolvedSuperSize > 1)
{
return new ErrorResponse(
"capture_source='scene_view' does not support super_size above 1. Remove 'super_size' or use capture_source='game_view'.");
}
if (!string.IsNullOrEmpty(cmd.batch))
{
return new ErrorResponse(
"capture_source='scene_view' does not support batch modes. Use capture_source='game_view' for batch capture.");
}
if (cmd.viewPosition.HasValue || cmd.viewRotation.HasValue)
{
return new ErrorResponse(
"capture_source='scene_view' does not support view_position/view_rotation. Use view_target to frame a Scene View object.");
}
if (!string.IsNullOrEmpty(cameraRef))
{
return new ErrorResponse(
"capture_source='scene_view' does not support camera selection. Remove 'camera' or use capture_source='game_view'.");
}
return CaptureSceneViewScreenshot(cmd, fileName, resolvedSuperSize, includeImage, maxResolution);
}
// Batch capture (e.g., "surround" for 6 angles around the scene)
if (!string.IsNullOrEmpty(cmd.batch))
{
if (cmd.batch.Equals("surround", StringComparison.OrdinalIgnoreCase))
return CaptureSurroundBatch(cmd);
if (cmd.batch.Equals("orbit", StringComparison.OrdinalIgnoreCase))
return CaptureOrbitBatch(cmd);
return new ErrorResponse($"Unknown batch mode: '{cmd.batch}'. Valid modes: 'surround', 'orbit'.");
}
// Positioned view-based capture (creates temp camera at view_position, aimed at view_target)
if ((cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null) || cmd.viewPosition.HasValue)
{
return CapturePositionedScreenshot(cmd);
}
// Batch mode warning
if (Application.isBatchMode)
{
McpLog.Warn("[ManageScene] Screenshot capture in batch mode uses camera-based fallback. Results may vary.");
}
// Resolve camera target
Camera targetCamera = null;
if (!string.IsNullOrEmpty(cameraRef))
{
targetCamera = ResolveCamera(cameraRef);
if (targetCamera == null)
{
return new ErrorResponse($"Camera '{cameraRef}' not found. Provide a Camera GameObject name, path, or instance ID.");
}
}
// When include_image is requested but no specific camera, use composited capture
// (ScreenCapture.CaptureScreenshotAsTexture) which captures UI Toolkit overlays.
// When a specific camera IS requested, use camera-based capture.
if (targetCamera != null)
{
if (!Application.isBatchMode) EnsureGameView();
string folderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
ScreenshotCaptureResult result = ScreenshotUtility.CaptureFromCameraToProjectFolder(
targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
includeImage: includeImage, maxResolution: maxResolution,
folderOverride: folderOverride);
if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
string message = $"Screenshot captured to '{result.ProjectRelativePath}' (camera: {targetCamera.name}).";
return new SuccessResponse(message, BuildScreenshotResponseData(result, targetCamera.name, includeImage));
}
if (includeImage && Application.isPlaying)
{
if (!Application.isBatchMode) EnsureGameView();
string folderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
ScreenshotCaptureResult result = ScreenshotUtility.CaptureComposited(
fileName, resolvedSuperSize, ensureUniqueFileName: true,
includeImage: true, maxResolution: maxResolution,
folderOverride: folderOverride);
if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
string cameraName = Camera.main != null ? Camera.main.name : "composited";
string message = $"Screenshot captured to '{result.ProjectRelativePath}' (camera: {cameraName}).";
return new SuccessResponse(message, BuildScreenshotResponseData(result, cameraName, includeImage: true));
}
if (includeImage)
{
// Not in play mode — fall back to camera-based capture
targetCamera = Camera.main;
if (targetCamera == null)
{
var allCams = UnityFindObjectsCompat.FindAll();
targetCamera = allCams.Length > 0 ? allCams[0] : null;
}
if (targetCamera == null)
{
return new ErrorResponse("No camera found in the scene. Add a Camera to use screenshot with include_image outside of Play mode.");
}
if (!Application.isBatchMode) EnsureGameView();
string folderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
ScreenshotCaptureResult result;
try
{
result = ScreenshotUtility.CaptureFromCameraToProjectFolder(
targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
includeImage: includeImage, maxResolution: maxResolution,
folderOverride: folderOverride);
}
catch (InvalidOperationException ex)
{
return new ErrorResponse(ex.Message);
}
if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
string message = $"Screenshot captured to '{result.ProjectRelativePath}' (camera: {targetCamera.name}).";
var data = new Dictionary
{
{ "path", result.ProjectRelativePath },
{ "fullPath", result.FullPath },
{ "superSize", result.SuperSize },
{ "isAsync", false },
{ "camera", targetCamera.name },
{ "captureSource", "game_view" },
};
if (includeImage && result.ImageBase64 != null)
{
data["imageBase64"] = result.ImageBase64;
data["imageWidth"] = result.ImageWidth;
data["imageHeight"] = result.ImageHeight;
}
return new SuccessResponse(message, BuildScreenshotResponseData(result, targetCamera.name, includeImage));
}
// Default path: ScreenCapture API for 2022.1+, camera fallback required on older versions.
#if !UNITY_2022_1_OR_NEWER
bool hasCameraFallback = Camera.main != null || UnityFindObjectsCompat.FindAll().Length > 0;
if (!hasCameraFallback)
{
return new ErrorResponse(
"No camera found in the scene. Screenshot capture on Unity versions before 2022.1 requires a Camera in the scene."
);
}
#endif
if (!Application.isBatchMode) EnsureGameView();
string defaultFolderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
ScreenshotCaptureResult defaultResult;
try
{
defaultResult = ScreenshotUtility.CaptureToProjectFolder(
fileName, resolvedSuperSize, ensureUniqueFileName: true,
folderOverride: defaultFolderOverride);
}
catch (InvalidOperationException ex)
{
return new ErrorResponse(ex.Message);
}
if (ScreenshotUtility.IsUnderAssets(defaultResult.ProjectRelativePath))
{
if (defaultResult.IsAsync)
ScheduleAssetImportWhenFileExists(defaultResult.ProjectRelativePath, defaultResult.FullPath, timeoutSeconds: 30.0);
else
AssetDatabase.ImportAsset(defaultResult.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
}
string verb = defaultResult.IsAsync ? "Screenshot requested" : "Screenshot captured";
return new SuccessResponse(
$"{verb} to '{defaultResult.ProjectRelativePath}'.",
new
{
path = defaultResult.ProjectRelativePath,
fullPath = defaultResult.FullPath,
superSize = defaultResult.SuperSize,
isAsync = defaultResult.IsAsync,
captureSource = "game_view",
}
);
}
catch (Exception e)
{
return new ErrorResponse($"Error capturing screenshot: {e.Message}");
}
}
private static Dictionary BuildScreenshotResponseData(
ScreenshotCaptureResult result,
string cameraName,
bool includeImage)
{
var data = new Dictionary
{
{ "path", result.ProjectRelativePath },
{ "fullPath", result.FullPath },
{ "superSize", result.SuperSize },
{ "isAsync", false },
{ "camera", cameraName },
{ "captureSource", "game_view" },
};
if (includeImage && result.ImageBase64 != null)
{
data["imageBase64"] = result.ImageBase64;
data["imageWidth"] = result.ImageWidth;
data["imageHeight"] = result.ImageHeight;
}
return data;
}
private static object CaptureSceneViewScreenshot(
SceneCommand cmd,
string fileName,
int resolvedSuperSize,
bool includeImage,
int maxResolution)
{
if (Application.isBatchMode)
{
return new ErrorResponse("capture_source='scene_view' is not supported in batch mode.");
}
var sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
{
return new ErrorResponse(
"No active Scene View found. Open a Scene View window first, then retry screenshot with capture_source='scene_view'.");
}
if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
var frameResult = FrameSceneView(new SceneCommand { sceneViewTarget = cmd.viewTarget });
if (frameResult is ErrorResponse)
{
return frameResult;
}
}
try
{
string sceneViewFolderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
ScreenshotCaptureResult result;
int viewportWidth;
int viewportHeight;
try
{
result = EditorWindowScreenshotUtility.CaptureSceneViewViewportToProject(
sceneView,
fileName,
resolvedSuperSize,
ensureUniqueFileName: true,
includeImage: includeImage,
maxResolution: maxResolution,
out viewportWidth,
out viewportHeight,
folderOverride: sceneViewFolderOverride);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Screenshot folder", StringComparison.Ordinal))
{
return new ErrorResponse(ex.Message);
}
if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
string sceneViewName = sceneView.titleContent?.text ?? "Scene";
var data = new Dictionary
{
{ "path", result.ProjectRelativePath },
{ "fullPath", result.FullPath },
{ "superSize", result.SuperSize },
{ "isAsync", false },
{ "camera", sceneView.camera != null ? sceneView.camera.name : "SceneCamera" },
{ "captureSource", "scene_view" },
{ "captureMode", "scene_view_viewport" },
{ "sceneViewName", sceneViewName },
{ "viewportWidth", viewportWidth },
{ "viewportHeight", viewportHeight },
};
if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
data["viewTarget"] = cmd.viewTarget;
}
if (includeImage && result.ImageBase64 != null)
{
data["imageBase64"] = result.ImageBase64;
data["imageWidth"] = result.ImageWidth;
data["imageHeight"] = result.ImageHeight;
}
return new SuccessResponse(
$"Scene View screenshot captured to '{result.ProjectRelativePath}' (scene view: {sceneViewName}).",
data);
}
catch (Exception e)
{
return new ErrorResponse($"Error capturing Scene View screenshot: {e.Message}");
}
}
///
/// Captures screenshots from 6 angles around scene bounds (or a view_target) for AI scene understanding.
/// Does NOT save to disk — returns all images as inline base64 PNGs. Always uses camera-based capture.
///
private static object CaptureSurroundBatch(SceneCommand cmd)
{
try
{
int maxRes = cmd.maxResolution ?? 480;
Vector3 center;
float radius;
// If view_target is provided, center on that target instead of scene bounds
if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
var targetPos3 = VectorParsing.ParseVector3(cmd.viewTarget);
if (targetPos3.HasValue)
{
center = targetPos3.Value;
radius = 5f;
}
else
{
Scene targetScene = EditorSceneManager.GetActiveScene();
var targetGo = ResolveGameObject(cmd.viewTarget, targetScene);
if (targetGo == null)
return new ErrorResponse($"view_target '{cmd.viewTarget}' not found for batch capture.");
Bounds targetBounds = new Bounds(targetGo.transform.position, Vector3.zero);
foreach (var r in targetGo.GetComponentsInChildren())
{
if (r != null && r.gameObject.activeInHierarchy) targetBounds.Encapsulate(r.bounds);
}
center = targetBounds.center;
radius = targetBounds.extents.magnitude * 2.5f;
radius = Mathf.Max(radius, 5f);
}
}
else
{
// Default: calculate combined bounds of all renderers in the scene
Bounds bounds = new Bounds(Vector3.zero, Vector3.zero);
bool hasBounds = false;
var renderers = UnityFindObjectsCompat.FindAll();
foreach (var r in renderers)
{
if (r == null || !r.gameObject.activeInHierarchy) continue;
if (!hasBounds)
{
bounds = r.bounds;
hasBounds = true;
}
else
{
bounds.Encapsulate(r.bounds);
}
}
if (!hasBounds)
return new ErrorResponse("No renderers found in the scene. Cannot determine scene bounds for batch capture.");
center = bounds.center;
radius = bounds.extents.magnitude * 2.5f;
radius = Mathf.Max(radius, 5f);
}
// Define 6 viewpoints: front, back, left, right, top, bird's-eye (45° elevated front-right)
var angles = new[]
{
("front", new Vector3(center.x, center.y, center.z - radius)),
("back", new Vector3(center.x, center.y, center.z + radius)),
("left", new Vector3(center.x - radius, center.y, center.z)),
("right", new Vector3(center.x + radius, center.y, center.z)),
("top", new Vector3(center.x, center.y + radius, center.z)),
("bird_eye", new Vector3(center.x + radius * 0.7f, center.y + radius * 0.7f, center.z - radius * 0.7f)),
};
// Create a temporary camera
var tempGo = new GameObject("__MCP_MultiAngle_Temp_Camera__");
Camera tempCam = tempGo.AddComponent();
tempCam.fieldOfView = 60f;
tempCam.nearClipPlane = 0.1f;
tempCam.farClipPlane = radius * 4f;
tempCam.clearFlags = CameraClearFlags.Skybox;
// Force material refresh once before capture loop
EditorApplication.QueuePlayerLoopUpdate();
SceneView.RepaintAll();
var tiles = new List();
var tileLabels = new List();
var shotMeta = new List