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(); // --- Prefab Redirection Check --- string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; if (!string.IsNullOrEmpty(targetPath) && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { // Allow 'create' (instantiate), 'find' (?), 'get_components' (?) if (action == "modify" || action == "set_component_property") { Debug.Log($"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset."); // Prepare params for ManageAsset.ModifyAsset JObject assetParams = new JObject(); assetParams["action"] = "modify"; // ManageAsset uses "modify" assetParams["path"] = targetPath; // Extract properties. // For 'set_component_property', combine componentName and componentProperties. // For 'modify', directly use componentProperties. JObject properties = null; if (action == "set_component_property") { string compName = @params["componentName"]?.ToString(); JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting if (string.IsNullOrEmpty(compName)) return Response.Error("Missing 'componentName' for 'set_component_property' on prefab."); if (compProps == null) return Response.Error($"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab."); properties = new JObject(); properties[compName] = compProps; } else // action == "modify" { properties = @params["componentProperties"] as JObject; if (properties == null) return Response.Error("Missing 'componentProperties' for 'modify' action on prefab."); } assetParams["properties"] = properties; // Call ManageAsset handler return ManageAsset.HandleCommand(assetParams); } else if (action == "delete" || action == "add_component" || action == "remove_component" || action == "get_components") // Added get_components here too { // Explicitly block other modifications on the prefab asset itself via manage_gameobject return Response.Error($"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command."); } // Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject. // No specific handling needed here, the code below will run. } // --- End Prefab Redirection Check --- 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 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; Debug.Log($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'"); string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}"); if (guids.Length == 0) { return Response.Error($"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 Response.Error($"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 Debug.Log($"[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. // We could also error here, but appending might be more user-friendly. Debug.LogWarning($"[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. } // Removed the early return error for missing .prefab ending. // 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 Debug.LogError($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."); return Response.Error($"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}'"); Debug.Log($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'."); } catch (Exception e) { return Response.Error($"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. Debug.LogWarning($"[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 return Response.Error("'name' parameter is required when creating a primitive."); // Name is essential createdNewObject = true; } 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 // Create empty GameObject { if (string.IsNullOrEmpty(name)) { return Response.Error("'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 Response.Error("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 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 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 Response.Error("'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)) { Debug.Log($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'"); finalPrefabPath += ".prefab"; } // Removed the error check here as we now ensure the extension exists // if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) // { // UnityEngine.Object.DestroyImmediate(newGo); // return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'"); // } 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(); // Refresh asset database to recognize the new folder Debug.Log($"[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 Response.Error($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."); } Debug.Log($"[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 Response.Error($"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."; } // Return data for the instance in the scene return Response.Success(successMessage, GetGameObjectData(finalInstance)); } 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 deletedObjects = new List(); foreach(var targetGo in targets) { if (targetGo != null) { string goName = targetGo.name; int goId = targetGo.GetInstanceID(); // Use Undo.DestroyObjectImmediate for undo support Undo.DestroyObjectImmediate(targetGo); deletedObjects.Add(new { name = goName, instanceID = goId }); } } if (deletedObjects.Count > 0) { string message = targets.Count == 1 ? $"GameObject '{deletedObjects[0].GetType().GetProperty("name").GetValue(deletedObjects[0])}' deleted successfully." : $"{deletedObjects.Count} GameObjects deleted successfully."; return Response.Success(message, deletedObjects); } else { // Should not happen if targets.Count > 0 initially, but defensive check return Response.Error("Failed to delete target GameObject(s)."); } } private static object FindGameObjects(JObject @params, JToken targetToken, string searchMethod) { bool findAll = @params["findAll"]?.ToObject() ?? false; List foundObjects = FindObjectsInternal(targetToken, searchMethod, findAll, @params); if (foundObjects.Count == 0) { return Response.Success("No matching GameObjects found.", new List()); } var results = foundObjects.Select(go => GetGameObjectData(go)).ToList(); return Response.Success($"Found {results.Count} GameObject(s).", results); } private static object GetComponentsFromTarget(string target, string searchMethod) { GameObject targetGo = FindObjectInternal(target, searchMethod); if (targetGo == null) { return Response.Error($"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'."); } try { Component[] components = targetGo.GetComponents(); var componentData = components.Select(c => GetComponentData(c)).ToList(); return Response.Success($"Retrieved {componentData.Count} components from '{targetGo.name}'.", componentData); } catch (Exception e) { return Response.Error($"Error getting components from '{targetGo.name}': {e.Message}"); } } private static object AddComponentToTarget(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"}'."); } string typeName = null; JObject properties = null; // Allow adding component specified directly or via componentsToAdd array (take first) if (@params["componentName"] != null) { typeName = @params["componentName"]?.ToString(); properties = @params["componentProperties"]?[typeName] as JObject; // Check if props are nested under name } else if (@params["componentsToAdd"] is JArray componentsToAddArray && componentsToAddArray.Count > 0) { var compToken = componentsToAddArray.First; 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)) { return Response.Error("Component type name ('componentName' or first element in 'componentsToAdd') is required."); } var addResult = AddComponentInternal(targetGo, typeName, properties); if (addResult != null) return addResult; // Return error EditorUtility.SetDirty(targetGo); return Response.Success($"Component '{typeName}' added to '{targetGo.name}'.", GetGameObjectData(targetGo)); // Return updated GO data } private static object RemoveComponentFromTarget(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"}'."); } string typeName = null; // Allow removing component specified directly or via componentsToRemove array (take first) if (@params["componentName"] != null) { typeName = @params["componentName"]?.ToString(); } else if (@params["componentsToRemove"] is JArray componentsToRemoveArray && componentsToRemoveArray.Count > 0) { typeName = componentsToRemoveArray.First?.ToString(); } if (string.IsNullOrEmpty(typeName)) { return Response.Error("Component type name ('componentName' or first element in 'componentsToRemove') is required."); } var removeResult = RemoveComponentInternal(targetGo, typeName); if (removeResult != null) return removeResult; // Return error EditorUtility.SetDirty(targetGo); return Response.Success($"Component '{typeName}' removed from '{targetGo.name}'.", GetGameObjectData(targetGo)); } private static object SetComponentPropertyOnTarget(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"}'."); } string compName = @params["componentName"]?.ToString(); JObject propertiesToSet = null; if (!string.IsNullOrEmpty(compName)) { // Properties might be directly under componentProperties or nested under the component name if (@params["componentProperties"] is JObject compProps) { propertiesToSet = compProps[compName] as JObject ?? compProps; // Allow flat or nested structure } } else { return Response.Error("'componentName' parameter is required."); } if (propertiesToSet == null || !propertiesToSet.HasValues) { return Response.Error("'componentProperties' dictionary for the specified component is required and cannot be empty."); } var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet); if (setResult != null) return setResult; // Return error EditorUtility.SetDirty(targetGo); return Response.Success($"Properties set for component '{compName}' on '{targetGo.name}'.", GetGameObjectData(targetGo)); } // --- Internal Helpers --- /// /// Finds a single GameObject based on token (ID, name, path) and search method. /// private static GameObject FindObjectInternal(JToken targetToken, string searchMethod, JObject findParams = null) { // If find_all is not explicitly false, we still want only one for most single-target operations. bool findAll = findParams?["findAll"]?.ToObject() ?? false; // If a specific target ID is given, always find just that one. if (targetToken?.Type == JTokenType.Integer || (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _))) { findAll = false; } List results = FindObjectsInternal(targetToken, searchMethod, findAll, findParams); return results.Count > 0 ? results[0] : null; } /// /// Core logic for finding GameObjects based on various criteria. /// private static List FindObjectsInternal(JToken targetToken, string searchMethod, bool findAll, JObject findParams = null) { List results = new List(); string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString(); // Use searchTerm if provided, else the target itself bool searchInChildren = findParams?["searchInChildren"]?.ToObject() ?? false; bool searchInactive = findParams?["searchInactive"]?.ToObject() ?? false; // Default search method if not specified if (string.IsNullOrEmpty(searchMethod)) { if (targetToken?.Type == JTokenType.Integer) searchMethod = "by_id"; else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/')) searchMethod = "by_path"; else searchMethod = "by_name"; // Default fallback } GameObject rootSearchObject = null; // If searching in children, find the initial target first if (searchInChildren && targetToken != null) { rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path"); // Find the root for child search if (rootSearchObject == null) { Debug.LogWarning($"[ManageGameObject.Find] Root object '{targetToken}' for child search not found."); return results; // Return empty if root not found } } switch (searchMethod) { case "by_id": if (int.TryParse(searchTerm, out int instanceId)) { // EditorUtility.InstanceIDToObject is slow, iterate manually if possible // GameObject obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject; var allObjects = GetAllSceneObjects(searchInactive); // More efficient GameObject obj = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId); if (obj != null) results.Add(obj); } break; case "by_name": var searchPoolName = rootSearchObject ? rootSearchObject.GetComponentsInChildren(searchInactive).Select(t => t.gameObject) : GetAllSceneObjects(searchInactive); results.AddRange(searchPoolName.Where(go => go.name == searchTerm)); break; case "by_path": // Path is relative to scene root or rootSearchObject Transform foundTransform = rootSearchObject ? rootSearchObject.transform.Find(searchTerm) : GameObject.Find(searchTerm)?.transform; if(foundTransform != null) results.Add(foundTransform.gameObject); break; case "by_tag": var searchPoolTag = rootSearchObject ? rootSearchObject.GetComponentsInChildren(searchInactive).Select(t => t.gameObject) : GetAllSceneObjects(searchInactive); results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm))); break; case "by_layer": var searchPoolLayer = rootSearchObject ? rootSearchObject.GetComponentsInChildren(searchInactive).Select(t => t.gameObject) : GetAllSceneObjects(searchInactive); if (int.TryParse(searchTerm, out int layerIndex)) { results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex)); } else { int namedLayer = LayerMask.NameToLayer(searchTerm); if(namedLayer != -1) results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer)); } break; case "by_component": Type componentType = FindType(searchTerm); if (componentType != null) { // Determine FindObjectsInactive based on the searchInactive flag FindObjectsInactive findInactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude; // Replace FindObjectsOfType with FindObjectsByType, specifying the sorting mode and inactive state var searchPoolComp = rootSearchObject ? rootSearchObject.GetComponentsInChildren(componentType, searchInactive).Select(c => (c as Component).gameObject) : UnityEngine.Object.FindObjectsByType(componentType, findInactive, FindObjectsSortMode.None).Select(c => (c as Component).gameObject); results.AddRange(searchPoolComp.Where(go => go != null)); // Ensure GO is valid } else { Debug.LogWarning($"[ManageGameObject.Find] Component type not found: {searchTerm}"); } break; case "by_id_or_name_or_path": // Helper method used internally if (int.TryParse(searchTerm, out int id)) { var allObjectsId = GetAllSceneObjects(true); // Search inactive for internal lookup GameObject objById = allObjectsId.FirstOrDefault(go => go.GetInstanceID() == id); if (objById != null) { results.Add(objById); break; } } GameObject objByPath = GameObject.Find(searchTerm); if (objByPath != null) { results.Add(objByPath); break; } var allObjectsName = GetAllSceneObjects(true); results.AddRange(allObjectsName.Where(go => go.name == searchTerm)); break; default: Debug.LogWarning($"[ManageGameObject.Find] Unknown search method: {searchMethod}"); break; } // If only one result is needed, return just the first one found. if (!findAll && results.Count > 1) { return new List { results[0] }; } return results.Distinct().ToList(); // Ensure uniqueness } // Helper to get all scene objects efficiently private static IEnumerable GetAllSceneObjects(bool includeInactive) { // SceneManager.GetActiveScene().GetRootGameObjects() is faster than FindObjectsOfType() var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects(); var allObjects = new List(); foreach(var root in rootObjects) { allObjects.AddRange(root.GetComponentsInChildren(includeInactive).Select(t=>t.gameObject)); } return allObjects; } /// /// Adds a component by type name and optionally sets properties. /// Returns null on success, or an error response object on failure. /// private static object AddComponentInternal(GameObject targetGo, string typeName, JObject properties) { Type componentType = FindType(typeName); if (componentType == null) { return Response.Error($"Component type '{typeName}' not found or is not a valid Component."); } if (!typeof(Component).IsAssignableFrom(componentType)) { return Response.Error($"Type '{typeName}' is not a Component."); } // Prevent adding Transform again if (componentType == typeof(Transform)) { return Response.Error("Cannot add another Transform component."); } // Check for 2D/3D physics component conflicts bool isAdding2DPhysics = typeof(Rigidbody2D).IsAssignableFrom(componentType) || typeof(Collider2D).IsAssignableFrom(componentType); bool isAdding3DPhysics = typeof(Rigidbody).IsAssignableFrom(componentType) || typeof(Collider).IsAssignableFrom(componentType); if (isAdding2DPhysics) { // Check if the GameObject already has any 3D Rigidbody or Collider if (targetGo.GetComponent() != null || targetGo.GetComponent() != null) { return Response.Error($"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider."); } } else if (isAdding3DPhysics) { // Check if the GameObject already has any 2D Rigidbody or Collider if (targetGo.GetComponent() != null || targetGo.GetComponent() != null) { return Response.Error($"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider."); } } // Check if component already exists (optional, depending on desired behavior) // if (targetGo.GetComponent(componentType) != null) { // return Response.Error($"Component '{typeName}' already exists on '{targetGo.name}'."); // } try { // Use Undo.AddComponent for undo support Component newComponent = Undo.AddComponent(targetGo, componentType); if (newComponent == null) { return Response.Error($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)."); } // Set properties if provided if (properties != null) { var setResult = SetComponentPropertiesInternal(targetGo, typeName, properties, newComponent); // Pass the new component instance if (setResult != null) { // If setting properties failed, maybe remove the added component? Undo.DestroyObjectImmediate(newComponent); return setResult; // Return the error from setting properties } } return null; // Success } catch (Exception e) { return Response.Error($"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}"); } } /// /// Removes a component by type name. /// Returns null on success, or an error response object on failure. /// private static object RemoveComponentInternal(GameObject targetGo, string typeName) { Type componentType = FindType(typeName); if (componentType == null) { return Response.Error($"Component type '{typeName}' not found for removal."); } // Prevent removing essential components if (componentType == typeof(Transform)) { return Response.Error("Cannot remove the Transform component."); } Component componentToRemove = targetGo.GetComponent(componentType); if (componentToRemove == null) { return Response.Error($"Component '{typeName}' not found on '{targetGo.name}' to remove."); } try { // Use Undo.DestroyObjectImmediate for undo support Undo.DestroyObjectImmediate(componentToRemove); return null; // Success } catch (Exception e) { return Response.Error($"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}"); } } /// /// Sets properties on a component. /// Returns null on success, or an error response object on failure. /// private static object SetComponentPropertiesInternal(GameObject targetGo, string compName, JObject propertiesToSet, Component targetComponentInstance = null) { Component targetComponent = targetComponentInstance ?? targetGo.GetComponent(compName); if (targetComponent == null) { return Response.Error($"Component '{compName}' not found on '{targetGo.name}' to set properties."); } Undo.RecordObject(targetComponent, "Set Component Properties"); foreach (var prop in propertiesToSet.Properties()) { string propName = prop.Name; JToken propValue = prop.Value; try { if (!SetProperty(targetComponent, propName, propValue)) { // Log warning if property could not be set Debug.LogWarning($"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch."); // Optionally return an error here instead of just logging // return Response.Error($"Could not set property '{propName}' on component '{compName}'."); } } catch (Exception e) { Debug.LogError($"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"); // Optionally return an error here // return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}"); } } EditorUtility.SetDirty(targetComponent); return null; // Success (or partial success if warnings were logged) } /// /// Helper to set a property or field via reflection, handling basic types. /// private static bool SetProperty(object target, string memberName, JToken value) { Type type = target.GetType(); BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; try { PropertyInfo propInfo = type.GetProperty(memberName, flags); if (propInfo != null && propInfo.CanWrite) { object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); if (convertedValue != null) { propInfo.SetValue(target, convertedValue); return true; } } else { FieldInfo fieldInfo = type.GetField(memberName, flags); if (fieldInfo != null) { object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); if (convertedValue != null) { fieldInfo.SetValue(target, convertedValue); return true; } } } } catch (Exception ex) { Debug.LogError($"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}"); } return false; } /// /// Simple JToken to Type conversion for common Unity types. /// private static object ConvertJTokenToType(JToken token, Type targetType) { try { // Basic types first if (targetType == typeof(string)) return token.ToObject(); if (targetType == typeof(int)) return token.ToObject(); if (targetType == typeof(float)) return token.ToObject(); if (targetType == typeof(bool)) return token.ToObject(); // Vector/Quaternion/Color types if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) return new Vector2(arrV2[0].ToObject(), arrV2[1].ToObject()); if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) return new Vector3(arrV3[0].ToObject(), arrV3[1].ToObject(), arrV3[2].ToObject()); if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) return new Vector4(arrV4[0].ToObject(), arrV4[1].ToObject(), arrV4[2].ToObject(), arrV4[3].ToObject()); if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) return new Quaternion(arrQ[0].ToObject(), arrQ[1].ToObject(), arrQ[2].ToObject(), arrQ[3].ToObject()); if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA return new Color(arrC[0].ToObject(), arrC[1].ToObject(), arrC[2].ToObject(), arrC.Count > 3 ? arrC[3].ToObject() : 1.0f); // Enum types if (targetType.IsEnum) return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing // Handle assigning Unity Objects (Assets, Scene Objects, Components) if (typeof(UnityEngine.Object).IsAssignableFrom(targetType)) { // CASE 1: Reference is a JSON Object specifying a scene object/component find criteria if (token is JObject refObject) { JToken findToken = refObject["find"]; string findMethod = refObject["method"]?.ToString() ?? "by_id_or_name_or_path"; // Default search string componentTypeName = refObject["component"]?.ToString(); if (findToken == null) { Debug.LogWarning($"[ConvertJTokenToType] Reference object missing 'find' property: {token}"); return null; } // Find the target GameObject // Pass 'searchInactive: true' for internal lookups to be more robust JObject findParams = new JObject(); findParams["searchInactive"] = true; GameObject foundGo = FindObjectInternal(findToken, findMethod, findParams); if (foundGo == null) { Debug.LogWarning($"[ConvertJTokenToType] Could not find GameObject specified by reference object: {token}"); return null; } // If a component type is specified, try to get it if (!string.IsNullOrEmpty(componentTypeName)) { Type compType = FindType(componentTypeName); if (compType == null) { Debug.LogWarning($"[ConvertJTokenToType] Could not find component type '{componentTypeName}' specified in reference object: {token}"); return null; } // Ensure the targetType is assignable from the found component type if (!targetType.IsAssignableFrom(compType)) { Debug.LogWarning($"[ConvertJTokenToType] Found component '{componentTypeName}' but it is not assignable to the target property type '{targetType.Name}'. Reference: {token}"); return null; } Component foundComp = foundGo.GetComponent(compType); if (foundComp == null) { Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but could not find component '{componentTypeName}' on it. Reference: {token}"); return null; } return foundComp; // Return the found component } else { // Otherwise, return the GameObject itself, ensuring it's assignable if (!targetType.IsAssignableFrom(typeof(GameObject))) { Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but it is not assignable to the target property type '{targetType.Name}' (component name was not specified). Reference: {token}"); return null; } return foundGo; // Return the found GameObject } } // CASE 2: Reference is a string, assume it's an asset path else if (token.Type == JTokenType.String) { string assetPath = token.ToString(); if (!string.IsNullOrEmpty(assetPath)) { // Attempt to load the asset from the provided path using the target type UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); if (loadedAsset != null) { return loadedAsset; // Return the loaded asset if successful } else { // Log a warning if the asset could not be found at the path Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists."); return null; } } else { // Handle cases where an empty string might be intended to clear the reference return null; // Assign null if the path is empty } } // CASE 3: Reference is null or empty JToken, assign null else if (token.Type == JTokenType.Null || string.IsNullOrEmpty(token.ToString())) { return null; } // CASE 4: Invalid format for Unity Object reference else { Debug.LogWarning($"[ConvertJTokenToType] Expected a string asset path or a reference object to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}"); return null; } } // Fallback: Try direct conversion (might work for other simple value types) // Be cautious here, this might throw errors for complex types not handled above try { return token.ToObject(targetType); } catch (Exception directConversionEx) { Debug.LogWarning($"[ConvertJTokenToType] Direct conversion failed for JToken '{token}' to type '{targetType.Name}': {directConversionEx.Message}. Specific handling might be needed."); return null; } } catch (Exception ex) { Debug.LogWarning($"[ConvertJTokenToType] Could not convert JToken '{token}' to type '{targetType.Name}': {ex.Message}"); return null; } } /// /// Helper to find a Type by name, searching relevant assemblies. /// private static Type FindType(string typeName) { if (string.IsNullOrEmpty(typeName)) return null; // Handle common Unity namespaces implicitly var type = Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") ?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.PhysicsModule") ?? // Example physics Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI") ?? // Example UI Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule") ?? Type.GetType(typeName); // Try direct name (if fully qualified or in mscorlib) if (type != null) return type; // If not found, search all loaded assemblies (slower) foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { type = assembly.GetType(typeName); if (type != null) return type; // Also check with namespaces if simple name given type = assembly.GetType("UnityEngine." + typeName); if (type != null) return type; type = assembly.GetType("UnityEditor." + typeName); if (type != null) return type; type = assembly.GetType("UnityEngine.UI." + typeName); if (type != null) return type; } return null; // Not found } /// /// Parses a JArray like [x, y, z] into a Vector3. /// private static Vector3? ParseVector3(JArray array) { if (array != null && array.Count == 3) { try { return new Vector3( array[0].ToObject(), array[1].ToObject(), array[2].ToObject() ); } catch { /* Ignore parsing errors */ } } return null; } // --- Data Serialization --- /// /// Creates a serializable representation of a GameObject. /// private static object GetGameObjectData(GameObject go) { if (go == null) return null; return new { name = go.name, instanceID = go.GetInstanceID(), tag = go.tag, layer = go.layer, activeSelf = go.activeSelf, activeInHierarchy = go.activeInHierarchy, isStatic = go.isStatic, scenePath = go.scene.path, // Identify which scene it belongs to transform = new // Serialize transform components carefully to avoid JSON issues { // Serialize Vector3 components individually to prevent self-referencing loops. // The default serializer can struggle with properties like Vector3.normalized. position = new { x = go.transform.position.x, y = go.transform.position.y, z = go.transform.position.z }, localPosition = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z }, rotation = new { x = go.transform.rotation.eulerAngles.x, y = go.transform.rotation.eulerAngles.y, z = go.transform.rotation.eulerAngles.z }, localRotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z }, scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z }, forward = new { x = go.transform.forward.x, y = go.transform.forward.y, z = go.transform.forward.z }, up = new { x = go.transform.up.x, y = go.transform.up.y, z = go.transform.up.z }, right = new { x = go.transform.right.x, y = go.transform.right.y, z = go.transform.right.z } }, parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent // Optionally include components, but can be large // components = go.GetComponents().Select(c => GetComponentData(c)).ToList() // Or just component names: componentNames = go.GetComponents().Select(c => c.GetType().FullName).ToList() }; } /// /// Creates a serializable representation of a Component. /// TODO: Add property serialization. /// private static object GetComponentData(Component c) { if (c == null) return null; var data = new Dictionary { { "typeName", c.GetType().FullName }, { "instanceID", c.GetInstanceID() } }; // Attempt to serialize public properties/fields (can be noisy/complex) /* try { var properties = new Dictionary(); var type = c.GetType(); BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; foreach (var prop in type.GetProperties(flags).Where(p => p.CanRead && p.GetIndexParameters().Length == 0)) { try { properties[prop.Name] = prop.GetValue(c); } catch { } } foreach (var field in type.GetFields(flags)) { try { properties[field.Name] = field.GetValue(c); } catch { } } data["properties"] = properties; } catch (Exception ex) { data["propertiesError"] = ex.Message; } */ return data; } } }