using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;
using UnityEditor.SceneManagement;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditorInternal;
using UnityMCP.Editor.Helpers; // For Response class
namespace UnityMCP.Editor.Tools
{
///
/// Handles GameObject manipulation within the current scene (CRUD, find, components).
///
public static class ManageGameObject
{
// --- Main Handler ---
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
// Parameters used by various actions
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
string name = @params["name"]?.ToString();
try
{
switch (action)
{
case "create":
return CreateGameObject(@params);
case "modify":
return ModifyGameObject(@params, targetToken, searchMethod);
case "delete":
return DeleteGameObject(targetToken, searchMethod);
case "find":
return FindGameObjects(@params, targetToken, searchMethod);
case "get_components":
string getCompTarget = targetToken?.ToString(); // Expect name, path, or ID string
if (getCompTarget == null) return Response.Error("'target' parameter required for get_components.");
return GetComponentsFromTarget(getCompTarget, searchMethod);
case "add_component":
return AddComponentToTarget(@params, targetToken, searchMethod);
case "remove_component":
return RemoveComponentFromTarget(@params, targetToken, searchMethod);
case "set_component_property":
return SetComponentPropertyOnTarget(@params, targetToken, searchMethod);
default:
return Response.Error($"Unknown action: '{action}'.");
}
}
catch (Exception e)
{
Debug.LogError($"[ManageGameObject] Action '{action}' failed: {e}");
return Response.Error($"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 Response.Error("'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
if (saveAsPrefab && string.IsNullOrEmpty(prefabPath))
{
return Response.Error("'prefabPath' is required when 'saveAsPrefab' is true.");
}
if (saveAsPrefab && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'");
}
string primitiveType = @params["primitiveType"]?.ToString();
GameObject newGo;
// Create primitive or empty GameObject
if (!string.IsNullOrEmpty(primitiveType))
{
try
{
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
newGo = GameObject.CreatePrimitive(type);
newGo.name = name; // Set name after creation
}
catch (ArgumentException)
{
return Response.Error($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}");
}
catch (Exception e)
{
return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}");
}
}
else
{
newGo = new GameObject(name);
}
// Record creation for Undo (initial object)
// Note: Prefab saving might have its own Undo implications or require different handling.
// PrefabUtility operations often handle their own Undo steps.
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{name}'");
// Set Parent (before potentially making it a prefab root)
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 Response.Error($"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))
{
// Similar logic as in ModifyGameObject for setting/creating tags
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
try {
newGo.tag = tagToSet;
} catch (UnityException ex) {
if (ex.Message.Contains("is not defined")) {
Debug.LogWarning($"[ManageGameObject.Create] Tag '{tagToSet}' not found. Attempting to create it.");
try {
InternalEditorUtility.AddTag(tagToSet);
newGo.tag = tagToSet; // Retry
Debug.Log($"[ManageGameObject.Create] Tag '{tagToSet}' created and assigned successfully.");
} catch (Exception innerEx) {
UnityEngine.Object.DestroyImmediate(newGo); // Clean up
return Response.Error($"Failed to create or assign tag '{tagToSet}' during creation: {innerEx.Message}.");
}
} else {
UnityEngine.Object.DestroyImmediate(newGo); // Clean up
return Response.Error($"Failed to set tag to '{tagToSet}' during creation: {ex.Message}.");
}
}
}
// 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
{
Debug.LogWarning($"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}");
}
}
}
// Save as Prefab if requested
GameObject prefabInstance = newGo; // Keep track of the instance potentially linked to the prefab
if (saveAsPrefab)
{
try
{
// Ensure directory exists
string directoryPath = System.IO.Path.GetDirectoryName(prefabPath);
if (!System.IO.Directory.Exists(directoryPath))
{
System.IO.Directory.CreateDirectory(directoryPath);
AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder
Debug.Log($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}");
}
// Save the GameObject as a prefab asset and connect the instance
// Use SaveAsPrefabAssetAndConnect to keep the instance in the scene linked
prefabInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, prefabPath, InteractionMode.UserAction);
if (prefabInstance == null)
{
// Destroy the original if saving failed somehow (shouldn't usually happen if path is valid)
UnityEngine.Object.DestroyImmediate(newGo);
return Response.Error($"Failed to save GameObject '{name}' as prefab at '{prefabPath}'. Check path and permissions.");
}
Debug.Log($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{prefabPath}' and instance connected.");
// Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it.
// EditorUtility.SetDirty(prefabInstance); // 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 Response.Error($"Error saving prefab '{prefabPath}': {e.Message}");
}
}
// Select the instance in the scene (which might now be a prefab instance)
Selection.activeGameObject = prefabInstance;
string successMessage = saveAsPrefab
? $"GameObject '{name}' created and saved as prefab to '{prefabPath}'."
: $"GameObject '{name}' created successfully in scene.";
// Return data for the instance in the scene
return Response.Success(successMessage, GetGameObjectData(prefabInstance));
}
private static object ModifyGameObject(JObject @params, JToken targetToken, string searchMethod)
{
GameObject targetGo = FindObjectInternal(targetToken, searchMethod);
if (targetGo == null)
{
return Response.Error($"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
string newName = @params["newName"]?.ToString();
if (!string.IsNullOrEmpty(newName) && targetGo.name != newName)
{
targetGo.name = newName;
modified = true;
}
// Change Parent
JToken newParentToken = @params["newParent"];
if (newParentToken != null)
{
GameObject newParentGo = FindObjectInternal(newParentToken, "by_id_or_name_or_path");
if (newParentGo == null && !(newParentToken.Type == JTokenType.Null || (newParentToken.Type == JTokenType.String && string.IsNullOrEmpty(newParentToken.ToString()))))
{
return Response.Error($"New parent ('{newParentToken}') not found.");
}
// Check for hierarchy loops
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
{
return Response.Error($"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
string newTag = @params["newTag"]?.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 (newTag != null && targetGo.tag != newTag)
{
// Ensure the tag is not empty, if empty, it means "Untagged" implicitly
string tagToSet = string.IsNullOrEmpty(newTag) ? "Untagged" : newTag;
try {
// First attempt to set the tag
targetGo.tag = tagToSet;
modified = true;
}
catch (UnityException ex)
{
// Check if the error is specifically because the tag doesn't exist
if (ex.Message.Contains("is not defined"))
{
Debug.LogWarning($"[ManageGameObject] Tag '{tagToSet}' not found. Attempting to create it.");
try
{
// Attempt to create the tag using internal utility
InternalEditorUtility.AddTag(tagToSet);
// Wait a frame maybe? Not strictly necessary but sometimes helps editor updates.
// yield return null; // Cannot yield here, editor script limitation
// Retry setting the tag immediately after creation
targetGo.tag = tagToSet;
modified = true; // Mark as modified on successful retry
Debug.Log($"[ManageGameObject] Tag '{tagToSet}' created and assigned successfully.");
}
catch (Exception innerEx)
{
// Handle failure during tag creation or the second assignment attempt
Debug.LogError($"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}");
return Response.Error($"Failed to create or assign tag '{tagToSet}': {innerEx.Message}. Check Tag Manager and permissions.");
}
}
else
{
// If the exception was for a different reason, return the original error
return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}.");
}
}
}
// Change Layer
JToken newLayerToken = @params["newLayer"];
if (newLayerToken != null)
{
int layer = -1;
if (newLayerToken.Type == JTokenType.Integer)
{
layer = newLayerToken.ToObject();
}
else if (newLayerToken.Type == JTokenType.String)
{
layer = LayerMask.NameToLayer(newLayerToken.ToString());
}
if (layer == -1 && newLayerToken.ToString() != "Default") // LayerMask.NameToLayer returns -1 for invalid names
{
return Response.Error($"Invalid layer specified: '{newLayerToken}'. Use a valid layer name or index.");
}
if (layer != -1 && targetGo.layer != layer)
{
targetGo.layer = layer;
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)
{
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)
{
// ... (parsing logic as in CreateGameObject) ...
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
if (@params["componentProperties"] is JObject componentPropertiesObj)
{
foreach (var prop in componentPropertiesObj.Properties())
{
string compName = prop.Name;
JObject propertiesToSet = prop.Value as JObject;
if (propertiesToSet != null)
{
var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
if (setResult != null) return setResult;
modified = true;
}
}
}
if (!modified)
{
return Response.Success($"No modifications applied to GameObject '{targetGo.name}'.", GetGameObjectData(targetGo));
}
EditorUtility.SetDirty(targetGo); // Mark scene as dirty
return Response.Success($"GameObject '{targetGo.name}' modified successfully.", GetGameObjectData(targetGo));
}
private static object DeleteGameObject(JToken targetToken, string searchMethod)
{
// Find potentially multiple objects if name/tag search is used without find_all=false implicitly
List targets = FindObjectsInternal(targetToken, searchMethod, true); // find_all=true for delete safety
if (targets.Count == 0)
{
return Response.Error($"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
}
List