352 lines
13 KiB
C#
352 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Tools
|
|
{
|
|
/// <summary>
|
|
/// Tool for managing components on GameObjects.
|
|
/// Actions: add, remove, set_property
|
|
///
|
|
/// This is a focused tool for component lifecycle operations.
|
|
/// For reading component data, use the unity://scene/gameobject/{id}/components resource.
|
|
/// </summary>
|
|
[McpForUnityTool("manage_components")]
|
|
public static class ManageComponents
|
|
{
|
|
/// <summary>
|
|
/// Handles the manage_components command.
|
|
/// </summary>
|
|
/// <param name="params">Command parameters</param>
|
|
/// <returns>Result of the component operation</returns>
|
|
public static object HandleCommand(JObject @params)
|
|
{
|
|
if (@params == null)
|
|
{
|
|
return new ErrorResponse("Parameters cannot be null.");
|
|
}
|
|
|
|
string action = ParamCoercion.CoerceString(@params["action"], null)?.ToLowerInvariant();
|
|
if (string.IsNullOrEmpty(action))
|
|
{
|
|
return new ErrorResponse("'action' parameter is required (add, remove, set_property).");
|
|
}
|
|
|
|
// Target resolution
|
|
JToken targetToken = @params["target"];
|
|
string searchMethod = ParamCoercion.CoerceString(@params["searchMethod"] ?? @params["search_method"], null);
|
|
|
|
if (targetToken == null)
|
|
{
|
|
return new ErrorResponse("'target' parameter is required.");
|
|
}
|
|
|
|
try
|
|
{
|
|
return action switch
|
|
{
|
|
"add" => AddComponent(@params, targetToken, searchMethod),
|
|
"remove" => RemoveComponent(@params, targetToken, searchMethod),
|
|
"set_property" => SetProperty(@params, targetToken, searchMethod),
|
|
_ => new ErrorResponse($"Unknown action: '{action}'. Supported actions: add, remove, set_property")
|
|
};
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
McpLog.Error($"[ManageComponents] Action '{action}' failed: {e}");
|
|
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
|
}
|
|
}
|
|
|
|
#region Action Implementations
|
|
|
|
private static object AddComponent(JObject @params, JToken targetToken, string searchMethod)
|
|
{
|
|
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
|
if (targetGo == null)
|
|
{
|
|
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
|
}
|
|
|
|
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
|
if (string.IsNullOrEmpty(componentTypeName))
|
|
{
|
|
return new ErrorResponse("'componentType' parameter is required for 'add' action.");
|
|
}
|
|
|
|
// Resolve component type using unified type resolver
|
|
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
|
|
if (type == null)
|
|
{
|
|
return new ErrorResponse($"Component type '{componentTypeName}' not found. Use a fully-qualified name if needed.");
|
|
}
|
|
|
|
// Use ComponentOps for the actual operation
|
|
Component newComponent = ComponentOps.AddComponent(targetGo, type, out string error);
|
|
if (newComponent == null)
|
|
{
|
|
return new ErrorResponse(error ?? $"Failed to add component '{componentTypeName}'.");
|
|
}
|
|
|
|
// Set properties if provided
|
|
JObject properties = @params["properties"] as JObject ?? @params["componentProperties"] as JObject;
|
|
if (properties != null && properties.HasValues)
|
|
{
|
|
// Record for undo before modifying properties
|
|
Undo.RecordObject(newComponent, "Modify Component Properties");
|
|
SetPropertiesOnComponent(newComponent, properties);
|
|
}
|
|
|
|
EditorUtility.SetDirty(targetGo);
|
|
MarkOwningSceneDirty(targetGo);
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Component '{componentTypeName}' added to '{targetGo.name}'.",
|
|
data = new
|
|
{
|
|
instanceID = targetGo.GetInstanceID(),
|
|
componentType = type.FullName,
|
|
componentInstanceID = newComponent.GetInstanceID()
|
|
}
|
|
};
|
|
}
|
|
|
|
private static object RemoveComponent(JObject @params, JToken targetToken, string searchMethod)
|
|
{
|
|
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
|
if (targetGo == null)
|
|
{
|
|
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
|
}
|
|
|
|
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
|
if (string.IsNullOrEmpty(componentTypeName))
|
|
{
|
|
return new ErrorResponse("'componentType' parameter is required for 'remove' action.");
|
|
}
|
|
|
|
// Resolve component type using unified type resolver
|
|
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
|
|
if (type == null)
|
|
{
|
|
return new ErrorResponse($"Component type '{componentTypeName}' not found.");
|
|
}
|
|
|
|
// Use ComponentOps for the actual operation
|
|
bool removed = ComponentOps.RemoveComponent(targetGo, type, out string error);
|
|
if (!removed)
|
|
{
|
|
return new ErrorResponse(error ?? $"Failed to remove component '{componentTypeName}'.");
|
|
}
|
|
|
|
EditorUtility.SetDirty(targetGo);
|
|
MarkOwningSceneDirty(targetGo);
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Component '{componentTypeName}' removed from '{targetGo.name}'.",
|
|
data = new
|
|
{
|
|
instanceID = targetGo.GetInstanceID()
|
|
}
|
|
};
|
|
}
|
|
|
|
private static object SetProperty(JObject @params, JToken targetToken, string searchMethod)
|
|
{
|
|
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
|
if (targetGo == null)
|
|
{
|
|
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
|
}
|
|
|
|
string componentType = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
|
if (string.IsNullOrEmpty(componentType))
|
|
{
|
|
return new ErrorResponse("'componentType' parameter is required for 'set_property' action.");
|
|
}
|
|
|
|
// Resolve component type using unified type resolver
|
|
Type type = UnityTypeResolver.ResolveComponent(componentType);
|
|
if (type == null)
|
|
{
|
|
return new ErrorResponse($"Component type '{componentType}' not found.");
|
|
}
|
|
|
|
Component component = targetGo.GetComponent(type);
|
|
if (component == null)
|
|
{
|
|
return new ErrorResponse($"Component '{componentType}' not found on '{targetGo.name}'.");
|
|
}
|
|
|
|
// Get property and value
|
|
string propertyName = ParamCoercion.CoerceString(@params["property"], null);
|
|
JToken valueToken = @params["value"];
|
|
|
|
// Support both single property or properties object
|
|
JObject properties = @params["properties"] as JObject;
|
|
|
|
if (string.IsNullOrEmpty(propertyName) && (properties == null || !properties.HasValues))
|
|
{
|
|
return new ErrorResponse("Either 'property'+'value' or 'properties' object is required for 'set_property' action.");
|
|
}
|
|
|
|
var errors = new List<string>();
|
|
|
|
try
|
|
{
|
|
Undo.RecordObject(component, $"Set property on {componentType}");
|
|
|
|
if (!string.IsNullOrEmpty(propertyName) && valueToken != null)
|
|
{
|
|
// Single property mode
|
|
var error = TrySetProperty(component, propertyName, valueToken);
|
|
if (error != null)
|
|
{
|
|
errors.Add(error);
|
|
}
|
|
}
|
|
|
|
if (properties != null && properties.HasValues)
|
|
{
|
|
// Multiple properties mode
|
|
foreach (var prop in properties.Properties())
|
|
{
|
|
var error = TrySetProperty(component, prop.Name, prop.Value);
|
|
if (error != null)
|
|
{
|
|
errors.Add(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
EditorUtility.SetDirty(component);
|
|
MarkOwningSceneDirty(targetGo);
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
return new
|
|
{
|
|
success = false,
|
|
message = $"Some properties failed to set on '{componentType}'.",
|
|
data = new
|
|
{
|
|
instanceID = targetGo.GetInstanceID(),
|
|
errors = errors
|
|
}
|
|
};
|
|
}
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Properties set on component '{componentType}' on '{targetGo.name}'.",
|
|
data = new
|
|
{
|
|
instanceID = targetGo.GetInstanceID()
|
|
}
|
|
};
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new ErrorResponse($"Error setting properties on component '{componentType}': {e.Message}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
/// <summary>
|
|
/// Marks the appropriate scene as dirty for the given GameObject.
|
|
/// Handles both regular scenes and prefab stages.
|
|
/// </summary>
|
|
private static void MarkOwningSceneDirty(GameObject targetGo)
|
|
{
|
|
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
if (prefabStage != null)
|
|
{
|
|
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
|
}
|
|
else
|
|
{
|
|
EditorSceneManager.MarkSceneDirty(targetGo.scene);
|
|
}
|
|
}
|
|
|
|
private static GameObject FindTarget(JToken targetToken, string searchMethod)
|
|
{
|
|
if (targetToken == null)
|
|
return null;
|
|
|
|
// Try instance ID first
|
|
if (targetToken.Type == JTokenType.Integer)
|
|
{
|
|
int instanceId = targetToken.Value<int>();
|
|
return GameObjectLookup.FindById(instanceId);
|
|
}
|
|
|
|
string targetStr = targetToken.ToString();
|
|
|
|
// Try parsing as instance ID
|
|
if (int.TryParse(targetStr, out int parsedId))
|
|
{
|
|
var byId = GameObjectLookup.FindById(parsedId);
|
|
if (byId != null)
|
|
return byId;
|
|
}
|
|
|
|
// Use GameObjectLookup for search
|
|
return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true);
|
|
}
|
|
|
|
private static void SetPropertiesOnComponent(Component component, JObject properties)
|
|
{
|
|
if (component == null || properties == null)
|
|
return;
|
|
|
|
var errors = new List<string>();
|
|
foreach (var prop in properties.Properties())
|
|
{
|
|
var error = TrySetProperty(component, prop.Name, prop.Value);
|
|
if (error != null)
|
|
errors.Add(error);
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
McpLog.Warn($"[ManageComponents] Some properties failed to set on {component.GetType().Name}: {string.Join(", ", errors)}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to set a property or field on a component.
|
|
/// Delegates to ComponentOps.SetProperty for unified implementation.
|
|
/// </summary>
|
|
private static string TrySetProperty(Component component, string propertyName, JToken value)
|
|
{
|
|
if (component == null || string.IsNullOrEmpty(propertyName))
|
|
return "Invalid component or property name";
|
|
|
|
if (ComponentOps.SetProperty(component, propertyName, value, out string error))
|
|
{
|
|
return null; // Success
|
|
}
|
|
|
|
McpLog.Warn($"[ManageComponents] {error}");
|
|
return error;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|