Files
Tomicz Engineering LLC deea7d2a56 Replace reflection with version-gated conditional compilation.
Addresses review feedback on the Unity 6.5 GetInstanceID migration:

- UnityObjectIdCompatExtensions: drop the reflective method lookup in
  favor of a simple #if UNITY_6000_5_OR_NEWER / #else split calling
  GetEntityId() or GetInstanceID() directly. Also wrap the class in
  the MCPForUnity.Runtime.Helpers namespace.
- UnityTypeConverters: remove the reflective EntityIdToObject probe and
  call EditorUtility.EntityIdToObject(EntityId) directly under the same
  version gate. Serialize entityID as EntityId.ToULong() rather than
  ToString(), since Unity's docs explicitly warn that the textual form
  is not a stable serialization contract.
- Drop #pragma warning disable 0619 from 22 files that no longer make
  any direct calls to obsolete APIs. The remaining 7 files still need
  it (FindObjectsOfType, InstanceIDToObject fallback) and are left as-is
  — those deprecations are out of scope for this PR.
- Add the MCPForUnity.Runtime.Helpers using to every file that calls
  GetInstanceIDCompat() now that the extension method lives in a
  namespace.
2026-04-11 22:55:45 +02:00

424 lines
17 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;
using MCPForUnity.Runtime.Helpers;
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}'.");
}
// When adding VFX-related components (ParticleSystem, LineRenderer, TrailRenderer),
// ensure the renderer has a material compatible with the active render pipeline.
// Without this, newly added ParticleSystems in URP/HDRP projects get Unity's default
// Built-in RP particle material, which renders as magenta.
EnsureVfxRendererMaterial(targetGo, newComponent);
// 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.GetInstanceIDCompat(),
componentType = type.FullName,
componentInstanceID = newComponent.GetInstanceIDCompat()
}
};
}
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.");
}
int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]);
if (componentIndex.HasValue)
{
var components = targetGo.GetComponents(type);
if (componentIndex.Value < 0 || componentIndex.Value >= components.Length)
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {components.Length} '{componentTypeName}' component(s).");
if (type == typeof(Transform) || type == typeof(RectTransform))
return new ErrorResponse("Cannot remove Transform or RectTransform components.");
Undo.DestroyObjectImmediate(components[componentIndex.Value]);
EditorUtility.SetDirty(targetGo);
MarkOwningSceneDirty(targetGo);
return new
{
success = true,
message = $"Component '{componentTypeName}' (index {componentIndex.Value}) removed from '{targetGo.name}'.",
data = new { instanceID = targetGo.GetInstanceIDCompat(), componentIndex = componentIndex.Value }
};
}
// Use ComponentOps for the actual operation (removes first instance)
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.GetInstanceIDCompat()
}
};
}
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.");
}
int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]);
Component component;
if (componentIndex.HasValue)
{
var components = targetGo.GetComponents(type);
if (componentIndex.Value < 0 || componentIndex.Value >= components.Length)
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {components.Length} '{componentType}' component(s).");
component = components[componentIndex.Value];
}
else
{
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.GetInstanceIDCompat(),
errors = errors
}
};
}
return new
{
success = true,
message = $"Properties set on component '{componentType}' on '{targetGo.name}'.",
data = new
{
instanceID = targetGo.GetInstanceIDCompat()
}
};
}
catch (Exception e)
{
return new ErrorResponse($"Error setting properties on component '{componentType}': {e.Message}");
}
}
#endregion
#region Helpers
/// <summary>
/// When a VFX-capable component is added (ParticleSystem, LineRenderer, TrailRenderer),
/// ensures its renderer material is valid for the active render pipeline.
/// This prevents magenta rendering in URP/HDRP projects where the default built-in
/// particle/line materials use incompatible shaders.
/// </summary>
private static void EnsureVfxRendererMaterial(GameObject go, Component addedComponent)
{
Renderer renderer = null;
if (addedComponent is ParticleSystem ps)
{
renderer = go.GetComponent<ParticleSystemRenderer>();
// Apply sensible defaults so newly added ParticleSystems aren't oversized.
// These are overridden by any subsequent particle_set_* calls.
RendererHelpers.SetSensibleParticleDefaults(ps);
}
else if (addedComponent is Renderer r)
{
// Covers LineRenderer, TrailRenderer, and any other Renderer subclass
renderer = r;
}
if (renderer != null)
{
var result = RendererHelpers.EnsureMaterial(renderer);
if (result.MaterialReplaced)
{
McpLog.Info($"[ManageComponents] Auto-assigned pipeline-compatible material to {renderer.GetType().Name} on '{go.name}' (reason: {result.ReplacementReason}).");
}
}
}
/// <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
}
}