#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Editor.Helpers; // For Response class
using MCPForUnity.Runtime.Serialization;
using Newtonsoft.Json; // Added for JsonSerializationException
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Compilation; // For CompilationPipeline
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MCPForUnity.Editor.Tools
{
///
/// Handles GameObject manipulation within the current scene (CRUD, find, components).
///
[McpForUnityTool("manage_gameobject", AutoRegister = false)]
public static class ManageGameObject
{
// Use shared serializer from helper class (backwards-compatible alias)
internal static JsonSerializer InputSerializer => UnityJsonSerializer.Instance;
// --- Main Handler ---
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return new ErrorResponse("Parameters cannot be null.");
}
string action = @params["action"]?.ToString().ToLower();
if (string.IsNullOrEmpty(action))
{
return new ErrorResponse("Action parameter is required.");
}
// Parameters used by various actions
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
string name = @params["name"]?.ToString();
// --- Usability Improvement: Alias 'name' to 'target' for modification actions ---
// If 'target' is missing but 'name' is provided, and we aren't creating a new object,
// assume the user meant "find object by name".
if (targetToken == null && !string.IsNullOrEmpty(name) && action != "create")
{
targetToken = name;
// We don't update @params["target"] because we use targetToken locally mostly,
// but some downstream methods might parse @params directly. Let's update @params too for safety.
@params["target"] = name;
}
// -------------------------------------------------------------------------------
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
string tag = @params["tag"]?.ToString();
string layer = @params["layer"]?.ToString();
JToken parentToken = @params["parent"];
// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
var componentPropsToken = @params["componentProperties"];
if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String)
{
try
{
var parsed = JObject.Parse(componentPropsToken.ToString());
@params["componentProperties"] = parsed;
}
catch (Exception e)
{
McpLog.Warn($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}");
}
}
// --- Prefab Asset Check ---
// Prefab assets require different tools. Only 'create' (instantiation) is valid here.
string targetPath =
targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
if (
!string.IsNullOrEmpty(targetPath)
&& targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)
&& action != "create" // Allow prefab instantiation
)
{
return new ErrorResponse(
$"Target '{targetPath}' is a prefab asset. " +
$"Use 'manage_asset' with action='modify' for prefab asset modifications, " +
$"or 'manage_prefabs' with action='open_stage' to edit the prefab in isolation mode."
);
}
// --- End Prefab Asset Check ---
try
{
switch (action)
{
// --- Primary lifecycle actions (kept in manage_gameobject) ---
case "create":
return CreateGameObject(@params);
case "modify":
return ModifyGameObject(@params, targetToken, searchMethod);
case "delete":
return DeleteGameObject(targetToken, searchMethod);
case "duplicate":
return DuplicateGameObject(@params, targetToken, searchMethod);
case "move_relative":
return MoveRelativeToObject(@params, targetToken, searchMethod);
default:
return new ErrorResponse($"Unknown action: '{action}'.");
}
}
catch (Exception e)
{
McpLog.Error($"[ManageGameObject] Action '{action}' failed: {e}");
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
}
}
// --- Action Implementations ---
private static object CreateGameObject(JObject @params)
{
string name = @params["name"]?.ToString();
if (string.IsNullOrEmpty(name))
{
return new ErrorResponse("'name' parameter is required for 'create' action.");
}
// Get prefab creation parameters
bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject() ?? false;
string prefabPath = @params["prefabPath"]?.ToString();
string tag = @params["tag"]?.ToString(); // Get tag for creation
string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check
GameObject newGo = null; // Initialize as null
// --- Try Instantiating Prefab First ---
string originalPrefabPath = prefabPath; // Keep original for messages
if (!string.IsNullOrEmpty(prefabPath))
{
// If no extension, search for the prefab by name
if (
!prefabPath.Contains("/")
&& !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)
)
{
string prefabNameOnly = prefabPath;
McpLog.Info(
$"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'"
);
string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}");
if (guids.Length == 0)
{
return new ErrorResponse(
$"Prefab named '{prefabNameOnly}' not found anywhere in the project."
);
}
else if (guids.Length > 1)
{
string foundPaths = string.Join(
", ",
guids.Select(g => AssetDatabase.GUIDToAssetPath(g))
);
return new ErrorResponse(
$"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path."
);
}
else // Exactly one found
{
prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Update prefabPath with the full path
McpLog.Info(
$"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'"
);
}
}
else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
// If it looks like a path but doesn't end with .prefab, assume user forgot it and append it.
McpLog.Warn(
$"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending."
);
prefabPath += ".prefab";
// Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that.
}
// The logic above now handles finding or assuming the .prefab extension.
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath);
if (prefabAsset != null)
{
try
{
// Instantiate the prefab, initially place it at the root
// Parent will be set later if specified
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
if (newGo == null)
{
// This might happen if the asset exists but isn't a valid GameObject prefab somehow
McpLog.Error(
$"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."
);
return new ErrorResponse(
$"Failed to instantiate prefab at '{prefabPath}'."
);
}
// Name the instance based on the 'name' parameter, not the prefab's default name
if (!string.IsNullOrEmpty(name))
{
newGo.name = name;
}
// Register Undo for prefab instantiation
Undo.RegisterCreatedObjectUndo(
newGo,
$"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'"
);
McpLog.Info(
$"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'."
);
}
catch (Exception e)
{
return new ErrorResponse(
$"Error instantiating prefab '{prefabPath}': {e.Message}"
);
}
}
else
{
// Only return error if prefabPath was specified but not found.
// If prefabPath was empty/null, we proceed to create primitive/empty.
McpLog.Warn(
$"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified."
);
// Do not return error here, allow fallback to primitive/empty creation
}
}
// --- Fallback: Create Primitive or Empty GameObject ---
bool createdNewObject = false; // Flag to track if we created (not instantiated)
if (newGo == null) // Only proceed if prefab instantiation didn't happen
{
if (!string.IsNullOrEmpty(primitiveType))
{
try
{
PrimitiveType type = (PrimitiveType)
Enum.Parse(typeof(PrimitiveType), primitiveType, true);
newGo = GameObject.CreatePrimitive(type);
// Set name *after* creation for primitives
if (!string.IsNullOrEmpty(name))
{
newGo.name = name;
}
else
{
UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak
return new ErrorResponse(
"'name' parameter is required when creating a primitive."
);
}
createdNewObject = true;
}
catch (ArgumentException)
{
return new ErrorResponse(
$"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"
);
}
catch (Exception e)
{
return new ErrorResponse(
$"Failed to create primitive '{primitiveType}': {e.Message}"
);
}
}
else // Create empty GameObject
{
if (string.IsNullOrEmpty(name))
{
return new ErrorResponse(
"'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive."
);
}
newGo = new GameObject(name);
createdNewObject = true;
}
// Record creation for Undo *only* if we created a new object
if (createdNewObject)
{
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
}
}
// --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists ---
if (newGo == null)
{
// Should theoretically not happen if logic above is correct, but safety check.
return new ErrorResponse("Failed to create or instantiate the GameObject.");
}
// Record potential changes to the existing prefab instance or the new GO
// Record transform separately in case parent changes affect it
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
Undo.RecordObject(newGo, "Set GameObject Properties");
// Set Parent
JToken parentToken = @params["parent"];
if (parentToken != null)
{
GameObject parentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); // Flexible parent finding
if (parentGo == null)
{
UnityEngine.Object.DestroyImmediate(newGo); // Clean up created object
return new ErrorResponse($"Parent specified ('{parentToken}') but not found.");
}
newGo.transform.SetParent(parentGo.transform, true); // worldPositionStays = true
}
// Set Transform
Vector3? position = ParseVector3(@params["position"] as JArray);
Vector3? rotation = ParseVector3(@params["rotation"] as JArray);
Vector3? scale = ParseVector3(@params["scale"] as JArray);
if (position.HasValue)
newGo.transform.localPosition = position.Value;
if (rotation.HasValue)
newGo.transform.localEulerAngles = rotation.Value;
if (scale.HasValue)
newGo.transform.localScale = scale.Value;
// Set Tag (added for create action)
if (!string.IsNullOrEmpty(tag))
{
// Check if tag exists first (Unity doesn't throw exceptions for undefined tags, just logs a warning)
if (tag != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tag))
{
McpLog.Info($"[ManageGameObject.Create] Tag '{tag}' not found. Creating it.");
try
{
InternalEditorUtility.AddTag(tag);
}
catch (Exception ex)
{
UnityEngine.Object.DestroyImmediate(newGo); // Clean up
return new ErrorResponse($"Failed to create tag '{tag}': {ex.Message}.");
}
}
try
{
newGo.tag = tag;
}
catch (Exception ex)
{
UnityEngine.Object.DestroyImmediate(newGo); // Clean up
return new ErrorResponse($"Failed to set tag to '{tag}' during creation: {ex.Message}.");
}
}
// Set Layer (new for create action)
string layerName = @params["layer"]?.ToString();
if (!string.IsNullOrEmpty(layerName))
{
int layerId = LayerMask.NameToLayer(layerName);
if (layerId != -1)
{
newGo.layer = layerId;
}
else
{
McpLog.Warn(
$"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer."
);
}
}
// Add Components
if (@params["componentsToAdd"] is JArray componentsToAddArray)
{
foreach (var compToken in componentsToAddArray)
{
string typeName = null;
JObject properties = null;
if (compToken.Type == JTokenType.String)
{
typeName = compToken.ToString();
}
else if (compToken is JObject compObj)
{
typeName = compObj["typeName"]?.ToString();
properties = compObj["properties"] as JObject;
}
if (!string.IsNullOrEmpty(typeName))
{
var addResult = AddComponentInternal(newGo, typeName, properties);
if (addResult != null) // Check if AddComponentInternal returned an error object
{
UnityEngine.Object.DestroyImmediate(newGo); // Clean up
return addResult; // Return the error response
}
}
else
{
McpLog.Warn(
$"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}"
);
}
}
}
// Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
GameObject finalInstance = newGo; // Use this for selection and return data
if (createdNewObject && saveAsPrefab)
{
string finalPrefabPath = prefabPath; // Use a separate variable for saving path
// This check should now happen *before* attempting to save
if (string.IsNullOrEmpty(finalPrefabPath))
{
// Clean up the created object before returning error
UnityEngine.Object.DestroyImmediate(newGo);
return new ErrorResponse(
"'prefabPath' is required when 'saveAsPrefab' is true and creating a new object."
);
}
// Ensure the *saving* path ends with .prefab
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
McpLog.Info(
$"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'"
);
finalPrefabPath += ".prefab";
}
try
{
// Ensure directory exists using the final saving path
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
if (
!string.IsNullOrEmpty(directoryPath)
&& !System.IO.Directory.Exists(directoryPath)
)
{
System.IO.Directory.CreateDirectory(directoryPath);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Refresh asset database to recognize the new folder
McpLog.Info(
$"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"
);
}
// Use SaveAsPrefabAssetAndConnect with the final saving path
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
newGo,
finalPrefabPath,
InteractionMode.UserAction
);
if (finalInstance == null)
{
// Destroy the original if saving failed somehow (shouldn't usually happen if path is valid)
UnityEngine.Object.DestroyImmediate(newGo);
return new ErrorResponse(
$"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."
);
}
McpLog.Info(
$"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected."
);
// Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it.
// EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect
}
catch (Exception e)
{
// Clean up the instance if prefab saving fails
UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt
return new ErrorResponse($"Error saving prefab '{finalPrefabPath}': {e.Message}");
}
}
// Select the instance in the scene (either prefab instance or newly created/saved one)
Selection.activeGameObject = finalInstance;
// Determine appropriate success message using the potentially updated or original path
string messagePrefabPath =
finalInstance == null
? originalPrefabPath
: AssetDatabase.GetAssetPath(
PrefabUtility.GetCorrespondingObjectFromSource(finalInstance)
?? (UnityEngine.Object)finalInstance
);
string successMessage;
if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab
{
successMessage =
$"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'.";
}
else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab
{
successMessage =
$"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'.";
}
else // Created new primitive or empty GO, didn't save as prefab
{
successMessage =
$"GameObject '{finalInstance.name}' created successfully in scene.";
}
// Use the new serializer helper
//return new SuccessResponse(successMessage, GetGameObjectData(finalInstance));
return new SuccessResponse(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance));
}
private static object ModifyGameObject(
JObject @params,
JToken targetToken,
string searchMethod
)
{
GameObject targetGo = FindObjectInternal(targetToken, searchMethod);
if (targetGo == null)
{
return new ErrorResponse(
$"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."
);
}
// Record state for Undo *before* modifications
Undo.RecordObject(targetGo.transform, "Modify GameObject Transform");
Undo.RecordObject(targetGo, "Modify GameObject Properties");
bool modified = false;
// Rename (using consolidated 'name' parameter)
string name = @params["name"]?.ToString();
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
{
targetGo.name = name;
modified = true;
}
// Change Parent (using consolidated 'parent' parameter)
JToken parentToken = @params["parent"];
if (parentToken != null)
{
GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path");
// Check for hierarchy loops
if (
newParentGo == null
&& !(
parentToken.Type == JTokenType.Null
|| (
parentToken.Type == JTokenType.String
&& string.IsNullOrEmpty(parentToken.ToString())
)
)
)
{
return new ErrorResponse($"New parent ('{parentToken}') not found.");
}
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
{
return new ErrorResponse(
$"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop."
);
}
if (targetGo.transform.parent != (newParentGo?.transform))
{
targetGo.transform.SetParent(newParentGo?.transform, true); // worldPositionStays = true
modified = true;
}
}
// Set Active State
bool? setActive = @params["setActive"]?.ToObject();
if (setActive.HasValue && targetGo.activeSelf != setActive.Value)
{
targetGo.SetActive(setActive.Value);
modified = true;
}
// Change Tag (using consolidated 'tag' parameter)
string tag = @params["tag"]?.ToString();
// Only attempt to change tag if a non-null tag is provided and it's different from the current one.
// Allow setting an empty string to remove the tag (Unity uses "Untagged").
if (tag != null && targetGo.tag != tag)
{
// Ensure the tag is not empty, if empty, it means "Untagged" implicitly
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
// Check if tag exists first (Unity doesn't throw exceptions for undefined tags, just logs a warning)
if (tagToSet != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagToSet))
{
McpLog.Info($"[ManageGameObject] Tag '{tagToSet}' not found. Creating it.");
try
{
InternalEditorUtility.AddTag(tagToSet);
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to create tag '{tagToSet}': {ex.Message}.");
}
}
try
{
targetGo.tag = tagToSet;
modified = true;
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}.");
}
}
// Change Layer (using consolidated 'layer' parameter)
string layerName = @params["layer"]?.ToString();
if (!string.IsNullOrEmpty(layerName))
{
int layerId = LayerMask.NameToLayer(layerName);
if (layerId == -1 && layerName != "Default")
{
return new ErrorResponse(
$"Invalid layer specified: '{layerName}'. Use a valid layer name."
);
}
if (layerId != -1 && targetGo.layer != layerId)
{
targetGo.layer = layerId;
modified = true;
}
}
// Transform Modifications
Vector3? position = ParseVector3(@params["position"] as JArray);
Vector3? rotation = ParseVector3(@params["rotation"] as JArray);
Vector3? scale = ParseVector3(@params["scale"] as JArray);
if (position.HasValue && targetGo.transform.localPosition != position.Value)
{
targetGo.transform.localPosition = position.Value;
modified = true;
}
if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value)
{
targetGo.transform.localEulerAngles = rotation.Value;
modified = true;
}
if (scale.HasValue && targetGo.transform.localScale != scale.Value)
{
targetGo.transform.localScale = scale.Value;
modified = true;
}
// --- Component Modifications ---
// Note: These might need more specific Undo recording per component
// Remove Components
if (@params["componentsToRemove"] is JArray componentsToRemoveArray)
{
foreach (var compToken in componentsToRemoveArray)
{
// ... (parsing logic as in CreateGameObject) ...
string typeName = compToken.ToString();
if (!string.IsNullOrEmpty(typeName))
{
var removeResult = RemoveComponentInternal(targetGo, typeName);
if (removeResult != null)
return removeResult; // Return error if removal failed
modified = true;
}
}
}
// Add Components (similar to create)
if (@params["componentsToAdd"] is JArray componentsToAddArrayModify)
{
foreach (var compToken in componentsToAddArrayModify)
{
string typeName = null;
JObject properties = null;
if (compToken.Type == JTokenType.String)
typeName = compToken.ToString();
else if (compToken is JObject compObj)
{
typeName = compObj["typeName"]?.ToString();
properties = compObj["properties"] as JObject;
}
if (!string.IsNullOrEmpty(typeName))
{
var addResult = AddComponentInternal(targetGo, typeName, properties);
if (addResult != null)
return addResult;
modified = true;
}
}
}
// Set Component Properties
var componentErrors = new List