deea7d2a56
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.
998 lines
41 KiB
C#
998 lines
41 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using MCPForUnity.Runtime.Helpers;
|
|
|
|
namespace MCPForUnity.Editor.Helpers
|
|
{
|
|
/// <summary>
|
|
/// Low-level component operations extracted from ManageGameObject and ManageComponents.
|
|
/// Provides pure C# operations without JSON parsing or response formatting.
|
|
/// </summary>
|
|
public static class ComponentOps
|
|
{
|
|
/// <summary>
|
|
/// Adds a component to a GameObject with Undo support.
|
|
/// </summary>
|
|
/// <param name="target">The target GameObject</param>
|
|
/// <param name="componentType">The type of component to add</param>
|
|
/// <param name="error">Error message if operation fails</param>
|
|
/// <returns>The added component, or null if failed</returns>
|
|
public static Component AddComponent(GameObject target, Type componentType, out string error)
|
|
{
|
|
error = null;
|
|
|
|
if (target == null)
|
|
{
|
|
error = "Target GameObject is null.";
|
|
return null;
|
|
}
|
|
|
|
if (componentType == null || !typeof(Component).IsAssignableFrom(componentType))
|
|
{
|
|
error = $"Type '{componentType?.Name ?? "null"}' is not a valid Component type.";
|
|
return null;
|
|
}
|
|
|
|
// Prevent adding duplicate Transform
|
|
if (componentType == typeof(Transform))
|
|
{
|
|
error = "Cannot add another Transform component.";
|
|
return null;
|
|
}
|
|
|
|
// Check for 2D/3D physics conflicts
|
|
string conflictError = CheckPhysicsConflict(target, componentType);
|
|
if (conflictError != null)
|
|
{
|
|
error = conflictError;
|
|
return null;
|
|
}
|
|
|
|
// Produce a clearer error when this component already exists and cannot be duplicated.
|
|
Component existingComponent = target.GetComponent(componentType);
|
|
if (existingComponent != null && !AllowsMultiple(target, componentType))
|
|
{
|
|
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
Component newComponent = Undo.AddComponent(target, componentType);
|
|
if (newComponent == null)
|
|
{
|
|
if (target.GetComponent(componentType) != null && !AllowsMultiple(target, componentType))
|
|
{
|
|
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
|
|
}
|
|
else
|
|
{
|
|
error = $"Failed to add component '{componentType.Name}' to '{target.name}'. Unity may restrict this component on the current target.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Apply default values for specific component types
|
|
ApplyDefaultValues(newComponent);
|
|
|
|
return newComponent;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Error adding component '{componentType.Name}': {ex.Message}";
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a component from a GameObject with Undo support.
|
|
/// </summary>
|
|
/// <param name="target">The target GameObject</param>
|
|
/// <param name="componentType">The type of component to remove</param>
|
|
/// <param name="error">Error message if operation fails</param>
|
|
/// <returns>True if component was removed successfully</returns>
|
|
public static bool RemoveComponent(GameObject target, Type componentType, out string error)
|
|
{
|
|
error = null;
|
|
|
|
if (target == null)
|
|
{
|
|
error = "Target GameObject is null.";
|
|
return false;
|
|
}
|
|
|
|
if (componentType == null)
|
|
{
|
|
error = "Component type is null.";
|
|
return false;
|
|
}
|
|
|
|
// Prevent removing Transform
|
|
if (componentType == typeof(Transform))
|
|
{
|
|
error = "Cannot remove Transform component.";
|
|
return false;
|
|
}
|
|
|
|
Component component = target.GetComponent(componentType);
|
|
if (component == null)
|
|
{
|
|
error = $"Component '{componentType.Name}' not found on '{target.name}'.";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
Undo.DestroyObjectImmediate(component);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Error removing component '{componentType.Name}': {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a property value on a component using reflection.
|
|
/// </summary>
|
|
/// <param name="component">The target component</param>
|
|
/// <param name="propertyName">The property or field name</param>
|
|
/// <param name="value">The value to set (JToken)</param>
|
|
/// <param name="error">Error message if operation fails</param>
|
|
/// <returns>True if property was set successfully</returns>
|
|
public static bool SetProperty(Component component, string propertyName, JToken value, out string error)
|
|
{
|
|
error = null;
|
|
|
|
if (component == null)
|
|
{
|
|
error = "Component is null.";
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(propertyName))
|
|
{
|
|
error = "Property name is null or empty.";
|
|
return false;
|
|
}
|
|
|
|
Type type = component.GetType();
|
|
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
|
string normalizedName = ParamCoercion.NormalizePropertyName(propertyName);
|
|
|
|
// UnityEventBase-derived types must be set via SerializedProperty, not reflection.
|
|
// Reflection creates a disconnected object that Unity's serialization layer doesn't track,
|
|
// causing m_PersistentCalls to be empty when the scene is saved.
|
|
Type memberType = ResolveMemberType(type, propertyName, normalizedName);
|
|
if (memberType != null && typeof(UnityEventBase).IsAssignableFrom(memberType))
|
|
{
|
|
return SetViaSerializedProperty(component, propertyName, normalizedName, value, out error);
|
|
}
|
|
|
|
// Try reflection first (property, field, then non-public serialized field)
|
|
if (TrySetViaReflection(component, type, propertyName, normalizedName, flags, value, out error))
|
|
return true;
|
|
|
|
// Reflection failed — fall back to SerializedProperty which handles arrays,
|
|
// custom serialization (e.g. UdonSharp), and types reflection can't convert.
|
|
string reflectionError = error;
|
|
if (SetViaSerializedProperty(component, propertyName, normalizedName, value, out error))
|
|
return true;
|
|
|
|
// Both paths failed. If reflection found the member but couldn't convert,
|
|
// report that (more useful than the SerializedProperty error).
|
|
// If reflection didn't find it at all, report the SerializedProperty error.
|
|
if (reflectionError != null && !reflectionError.Contains("not found"))
|
|
error = reflectionError;
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TrySetViaReflection(object component, Type type, string propertyName, string normalizedName, BindingFlags flags, JToken value, out string error)
|
|
{
|
|
error = null;
|
|
|
|
// Skip reflection for UnityEngine.Object types with JObject values
|
|
// so SerializedProperty can resolve guid/spriteName/fileID forms.
|
|
bool isJObjectValue = value != null && value.Type == JTokenType.Object;
|
|
|
|
// Try property first
|
|
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
|
|
?? type.GetProperty(normalizedName, flags);
|
|
if (propInfo != null && propInfo.CanWrite)
|
|
{
|
|
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType))
|
|
{
|
|
// Let SerializedProperty path handle complex object references.
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType);
|
|
if (convertedValue == null && value.Type != JTokenType.Null)
|
|
{
|
|
error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'.";
|
|
return false;
|
|
}
|
|
propInfo.SetValue(component, convertedValue);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Failed to set property '{propertyName}': {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try field
|
|
FieldInfo fieldInfo = type.GetField(propertyName, flags)
|
|
?? type.GetField(normalizedName, flags);
|
|
if (fieldInfo != null && !fieldInfo.IsInitOnly)
|
|
{
|
|
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
|
|
{
|
|
// Let SerializedProperty path handle complex object references.
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
|
|
if (convertedValue == null && value.Type != JTokenType.Null)
|
|
{
|
|
error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
|
|
return false;
|
|
}
|
|
fieldInfo.SetValue(component, convertedValue);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Failed to set field '{propertyName}': {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try non-public serialized fields — traverse inheritance hierarchy
|
|
fieldInfo = FindSerializedFieldInHierarchy(type, propertyName)
|
|
?? FindSerializedFieldInHierarchy(type, normalizedName);
|
|
if (fieldInfo != null)
|
|
{
|
|
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
|
|
{
|
|
// Let SerializedProperty path handle complex object references.
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
|
|
if (convertedValue == null && value.Type != JTokenType.Null)
|
|
{
|
|
error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
|
|
return false;
|
|
}
|
|
fieldInfo.SetValue(component, convertedValue);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Failed to set serialized field '{propertyName}': {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
error = $"Property or field '{propertyName}' not found on component '{type.Name}'.";
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all public properties and fields from a component type.
|
|
/// </summary>
|
|
public static List<string> GetAccessibleMembers(Type componentType)
|
|
{
|
|
var members = new List<string>();
|
|
if (componentType == null) return members;
|
|
|
|
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
|
|
|
|
foreach (var prop in componentType.GetProperties(flags))
|
|
{
|
|
if (prop.CanWrite && prop.GetSetMethod() != null)
|
|
{
|
|
members.Add(prop.Name);
|
|
}
|
|
}
|
|
|
|
foreach (var field in componentType.GetFields(flags))
|
|
{
|
|
if (!field.IsInitOnly)
|
|
{
|
|
members.Add(field.Name);
|
|
}
|
|
}
|
|
|
|
// Include private [SerializeField] fields - traverse inheritance hierarchy
|
|
// Type.GetFields with NonPublic only returns fields declared directly on that type,
|
|
// so we need to walk up the chain to find inherited private serialized fields
|
|
var seenFieldNames = new HashSet<string>(members); // Avoid duplicates with public fields
|
|
Type currentType = componentType;
|
|
while (currentType != null && currentType != typeof(object))
|
|
{
|
|
foreach (var field in currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
|
{
|
|
if (field.GetCustomAttribute<SerializeField>() != null && !seenFieldNames.Contains(field.Name))
|
|
{
|
|
members.Add(field.Name);
|
|
seenFieldNames.Add(field.Name);
|
|
}
|
|
}
|
|
currentType = currentType.BaseType;
|
|
}
|
|
|
|
members.Sort();
|
|
return members;
|
|
}
|
|
|
|
// --- Private Helpers ---
|
|
|
|
/// <summary>
|
|
/// Searches for a non-public [SerializeField] field through the entire inheritance hierarchy.
|
|
/// Type.GetField() with NonPublic only returns fields declared directly on that type,
|
|
/// so this method walks up the chain to find inherited private serialized fields.
|
|
/// </summary>
|
|
internal static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName)
|
|
{
|
|
if (type == null || string.IsNullOrEmpty(fieldName))
|
|
return null;
|
|
|
|
BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
|
Type currentType = type;
|
|
|
|
// Walk up the inheritance chain
|
|
while (currentType != null && currentType != typeof(object))
|
|
{
|
|
// Search for the field on this specific type (case-insensitive)
|
|
foreach (var field in currentType.GetFields(privateFlags))
|
|
{
|
|
if (string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase) &&
|
|
field.GetCustomAttribute<SerializeField>() != null)
|
|
{
|
|
return field;
|
|
}
|
|
}
|
|
currentType = currentType.BaseType;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string CheckPhysicsConflict(GameObject target, Type componentType)
|
|
{
|
|
bool isAdding2DPhysics =
|
|
typeof(Rigidbody2D).IsAssignableFrom(componentType) ||
|
|
typeof(Collider2D).IsAssignableFrom(componentType);
|
|
|
|
bool isAdding3DPhysics =
|
|
typeof(Rigidbody).IsAssignableFrom(componentType) ||
|
|
typeof(Collider).IsAssignableFrom(componentType);
|
|
|
|
if (isAdding2DPhysics)
|
|
{
|
|
if (target.GetComponent<Rigidbody>() != null || target.GetComponent<Collider>() != null)
|
|
{
|
|
return $"Cannot add 2D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 3D Rigidbody or Collider.";
|
|
}
|
|
}
|
|
else if (isAdding3DPhysics)
|
|
{
|
|
if (target.GetComponent<Rigidbody2D>() != null || target.GetComponent<Collider2D>() != null)
|
|
{
|
|
return $"Cannot add 3D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 2D Rigidbody or Collider.";
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void ApplyDefaultValues(Component component)
|
|
{
|
|
// Default newly added Lights to Directional
|
|
if (component is Light light)
|
|
{
|
|
light.type = LightType.Directional;
|
|
}
|
|
}
|
|
|
|
private static bool AllowsMultiple(GameObject target, Type componentType)
|
|
{
|
|
if (target == null || componentType == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Attribute.IsDefined(componentType, typeof(DisallowMultipleComponent), inherit: true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --- UnityEvent SerializedProperty support ---
|
|
|
|
private static Type ResolveMemberType(Type componentType, string propertyName, string normalizedName)
|
|
{
|
|
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
|
|
|
PropertyInfo propInfo = componentType.GetProperty(propertyName, flags)
|
|
?? componentType.GetProperty(normalizedName, flags);
|
|
if (propInfo != null)
|
|
return propInfo.PropertyType;
|
|
|
|
FieldInfo fieldInfo = componentType.GetField(propertyName, flags)
|
|
?? componentType.GetField(normalizedName, flags);
|
|
if (fieldInfo != null)
|
|
return fieldInfo.FieldType;
|
|
|
|
fieldInfo = FindSerializedFieldInHierarchy(componentType, propertyName)
|
|
?? FindSerializedFieldInHierarchy(componentType, normalizedName);
|
|
if (fieldInfo != null)
|
|
return fieldInfo.FieldType;
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool SetViaSerializedProperty(Component component, string propertyName, string normalizedName, JToken value, out string error)
|
|
{
|
|
error = null;
|
|
using var so = new SerializedObject(component);
|
|
|
|
SerializedProperty prop = so.FindProperty(propertyName)
|
|
?? so.FindProperty(normalizedName);
|
|
if (prop == null)
|
|
{
|
|
error = $"SerializedProperty '{propertyName}' not found on component '{component.GetType().Name}'.";
|
|
return false;
|
|
}
|
|
|
|
if (!SetSerializedPropertyRecursive(prop, value, out error, 0))
|
|
return false;
|
|
|
|
so.ApplyModifiedProperties();
|
|
|
|
// Readback verification for ObjectReference — these can silently fail
|
|
if (prop.propertyType == SerializedPropertyType.ObjectReference
|
|
&& value != null
|
|
&& !(value is JValue jv && jv.Type == JTokenType.Null))
|
|
{
|
|
so.Update();
|
|
var verifyProp = so.FindProperty(propertyName)
|
|
?? so.FindProperty(normalizedName);
|
|
if (verifyProp != null
|
|
&& verifyProp.propertyType == SerializedPropertyType.ObjectReference
|
|
&& verifyProp.objectReferenceValue == null)
|
|
{
|
|
error = $"Property '{propertyName}' was set but the object reference did not persist. " +
|
|
"Check that the referenced object exists and is the correct type.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JToken value, out string error, int depth)
|
|
{
|
|
error = null;
|
|
const int MaxDepth = 20;
|
|
if (depth > MaxDepth)
|
|
{
|
|
error = $"Maximum recursion depth ({MaxDepth}) exceeded.";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Array + JArray
|
|
if (prop.isArray && prop.propertyType != SerializedPropertyType.String && value is JArray jArray)
|
|
{
|
|
prop.arraySize = jArray.Count;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
prop.serializedObject.Update();
|
|
|
|
for (int i = 0; i < jArray.Count; i++)
|
|
{
|
|
var element = prop.GetArrayElementAtIndex(i);
|
|
if (!SetSerializedPropertyRecursive(element, jArray[i], out error, depth + 1))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Generic (struct/class) + JObject
|
|
if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && value is JObject jObj)
|
|
{
|
|
foreach (var kvp in jObj)
|
|
{
|
|
var child = FindPropertyRelativeFuzzy(prop, kvp.Key);
|
|
if (child == null)
|
|
{
|
|
error = $"Sub-property '{kvp.Key}' not found under '{prop.propertyPath}'.";
|
|
return false;
|
|
}
|
|
if (!SetSerializedPropertyRecursive(child, kvp.Value, out error, depth + 1))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ObjectReference
|
|
if (prop.propertyType == SerializedPropertyType.ObjectReference)
|
|
return SetObjectReference(prop, value, out error);
|
|
|
|
// Leaf types
|
|
switch (prop.propertyType)
|
|
{
|
|
case SerializedPropertyType.Integer:
|
|
if (value == null || value.Type == JTokenType.Null
|
|
|| (value.Type != JTokenType.Integer && value.Type != JTokenType.Float
|
|
&& !long.TryParse(value.ToString(), out _)))
|
|
{
|
|
error = "Expected integer value.";
|
|
return false;
|
|
}
|
|
if (prop.type == "long")
|
|
prop.longValue = ParamCoercion.CoerceLong(value, 0);
|
|
else
|
|
prop.intValue = ParamCoercion.CoerceInt(value, 0);
|
|
return true;
|
|
|
|
case SerializedPropertyType.Boolean:
|
|
if (value == null || value.Type == JTokenType.Null)
|
|
{
|
|
error = "Expected boolean value.";
|
|
return false;
|
|
}
|
|
prop.boolValue = ParamCoercion.CoerceBool(value, false);
|
|
return true;
|
|
|
|
case SerializedPropertyType.Float:
|
|
float floatVal = ParamCoercion.CoerceFloat(value, float.NaN);
|
|
if (float.IsNaN(floatVal))
|
|
{
|
|
error = "Expected float value.";
|
|
return false;
|
|
}
|
|
prop.floatValue = floatVal;
|
|
return true;
|
|
|
|
case SerializedPropertyType.String:
|
|
prop.stringValue = value == null || value.Type == JTokenType.Null ? string.Empty : value.ToString();
|
|
return true;
|
|
|
|
case SerializedPropertyType.Enum:
|
|
return SetEnum(prop, value, out error);
|
|
|
|
default:
|
|
error = $"Unsupported SerializedPropertyType: {prop.propertyType} at '{prop.propertyPath}'.";
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Error setting '{prop.propertyPath}': {ex.Message}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static bool SetObjectReference(SerializedProperty prop, JToken value, out string error)
|
|
{
|
|
error = null;
|
|
|
|
if (value == null || value.Type == JTokenType.Null)
|
|
{
|
|
prop.objectReferenceValue = null;
|
|
return true;
|
|
}
|
|
|
|
if (value.Type == JTokenType.Integer)
|
|
{
|
|
int id = value.Value<int>();
|
|
var resolved = GameObjectLookup.ResolveInstanceID(id);
|
|
if (resolved == null)
|
|
{
|
|
error = $"No object found with instanceID {id}.";
|
|
return false;
|
|
}
|
|
return AssignObjectReference(prop, resolved, null, out error);
|
|
}
|
|
|
|
if (value is JObject jObj)
|
|
{
|
|
// Optional component type filter — e.g. {"instanceID": 123, "component": "Button"}
|
|
string componentFilter = jObj["component"]?.ToString();
|
|
|
|
var idToken = jObj["instanceID"];
|
|
if (idToken != null)
|
|
{
|
|
int id = ParamCoercion.CoerceInt(idToken, 0);
|
|
var resolved = GameObjectLookup.ResolveInstanceID(id);
|
|
if (resolved == null)
|
|
{
|
|
error = $"No object found with instanceID {id}.";
|
|
return false;
|
|
}
|
|
return AssignObjectReference(prop, resolved, componentFilter, out error);
|
|
}
|
|
|
|
var guidToken = jObj["guid"];
|
|
if (guidToken != null)
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(guidToken.ToString());
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
error = $"No asset found for GUID '{guidToken}'.";
|
|
return false;
|
|
}
|
|
|
|
var spriteNameToken = jObj["spriteName"];
|
|
if (spriteNameToken != null)
|
|
{
|
|
string spriteName = spriteNameToken.ToString();
|
|
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
|
|
var originalRef = prop.objectReferenceValue;
|
|
foreach (var asset in allAssets)
|
|
{
|
|
if (asset is Sprite sprite && sprite.name == spriteName)
|
|
{
|
|
prop.objectReferenceValue = sprite;
|
|
if (prop.objectReferenceValue != null)
|
|
return true;
|
|
// Unity rejected the type — restore and report
|
|
prop.objectReferenceValue = originalRef;
|
|
error = $"Sprite '{spriteName}' found but is not compatible with the property type.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
|
|
return false;
|
|
}
|
|
|
|
var fileIdToken = jObj["fileID"];
|
|
if (fileIdToken != null)
|
|
{
|
|
long targetFileId = fileIdToken.Value<long>();
|
|
if (targetFileId != 0)
|
|
{
|
|
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
|
|
var originalRef = prop.objectReferenceValue;
|
|
foreach (var asset in allAssets)
|
|
{
|
|
if (asset is Sprite sprite)
|
|
{
|
|
long spriteFileId = GetSpriteFileId(sprite);
|
|
if (spriteFileId == targetFileId)
|
|
{
|
|
prop.objectReferenceValue = sprite;
|
|
if (prop.objectReferenceValue != null)
|
|
return true;
|
|
prop.objectReferenceValue = originalRef;
|
|
error = $"Sprite with fileID '{targetFileId}' found but is not compatible with the property type.";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
|
|
return false;
|
|
}
|
|
|
|
var loaded = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
|
|
return AssignObjectReference(prop, loaded, componentFilter, out error);
|
|
}
|
|
|
|
var pathToken = jObj["path"];
|
|
if (pathToken != null)
|
|
{
|
|
string sanitized = AssetPathUtility.SanitizeAssetPath(pathToken.ToString());
|
|
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
|
|
if (resolved == null)
|
|
{
|
|
error = $"No asset found at path '{pathToken}'.";
|
|
return false;
|
|
}
|
|
return AssignObjectReference(prop, resolved, componentFilter, out error);
|
|
}
|
|
|
|
var nameToken = jObj["name"];
|
|
if (nameToken != null)
|
|
{
|
|
return ResolveSceneObjectByName(prop, nameToken.ToString(), componentFilter, out error);
|
|
}
|
|
|
|
error = "Object reference must contain 'instanceID', 'guid', 'path', or 'name'.";
|
|
return false;
|
|
}
|
|
|
|
if (value.Type == JTokenType.String)
|
|
{
|
|
string strVal = value.ToString();
|
|
|
|
// Try as instanceID if the string is purely numeric
|
|
if (int.TryParse(strVal, out int parsedId))
|
|
{
|
|
var resolved = GameObjectLookup.ResolveInstanceID(parsedId);
|
|
if (resolved != null)
|
|
return AssignObjectReference(prop, resolved, null, out error);
|
|
// Not a valid instanceID — fall through to path/name resolution
|
|
}
|
|
|
|
if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || strVal.Contains("/"))
|
|
{
|
|
string sanitized = AssetPathUtility.SanitizeAssetPath(strVal);
|
|
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
|
|
if (resolved == null)
|
|
{
|
|
error = $"No asset found at path '{strVal}'.";
|
|
return false;
|
|
}
|
|
return AssignObjectReference(prop, resolved, null, out error);
|
|
}
|
|
|
|
// Try as asset GUID (32-char hex string)
|
|
if (strVal.Length == 32 && IsHexString(strVal))
|
|
{
|
|
string assetPath = AssetDatabase.GUIDToAssetPath(strVal);
|
|
if (!string.IsNullOrEmpty(assetPath))
|
|
{
|
|
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
|
if (resolved != null)
|
|
return AssignObjectReference(prop, resolved, null, out error);
|
|
}
|
|
}
|
|
|
|
// Fall back to scene hierarchy lookup by name.
|
|
return ResolveSceneObjectByName(prop, strVal, null, out error);
|
|
}
|
|
|
|
error = $"Unsupported object reference format: {value.Type}.";
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns a resolved object to a SerializedProperty, with automatic component fallback.
|
|
/// If the resolved object is a GameObject but the property expects a Component type,
|
|
/// searches the GameObject's components for a compatible one.
|
|
/// Optionally filters by component type name (e.g. "Button", "Rigidbody").
|
|
/// </summary>
|
|
private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.Object resolved, string componentFilter, out string error)
|
|
{
|
|
error = null;
|
|
if (resolved == null)
|
|
{
|
|
error = "Resolved object is null.";
|
|
return false;
|
|
}
|
|
|
|
// If a component filter is specified and the resolved object is a GameObject,
|
|
// find the specific component by type name.
|
|
if (!string.IsNullOrEmpty(componentFilter) && resolved is GameObject filterGo)
|
|
{
|
|
var components = filterGo.GetComponents<Component>();
|
|
foreach (var comp in components)
|
|
{
|
|
if (comp == null) continue;
|
|
if (string.Equals(comp.GetType().Name, componentFilter, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(comp.GetType().FullName, componentFilter, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
prop.objectReferenceValue = comp;
|
|
if (prop.objectReferenceValue != null)
|
|
return true;
|
|
}
|
|
}
|
|
error = $"Component '{componentFilter}' not found on GameObject '{filterGo.name}'.";
|
|
return false;
|
|
}
|
|
|
|
// Try direct assignment first
|
|
prop.objectReferenceValue = resolved;
|
|
if (prop.objectReferenceValue != null)
|
|
return true;
|
|
|
|
// Sub-asset fallback: e.g., Texture2D → Sprite
|
|
string subAssetPath = AssetDatabase.GetAssetPath(resolved);
|
|
if (!string.IsNullOrEmpty(subAssetPath))
|
|
{
|
|
var subAssets = AssetDatabase.LoadAllAssetsAtPath(subAssetPath);
|
|
UnityEngine.Object match = null;
|
|
int matchCount = 0;
|
|
foreach (var sub in subAssets)
|
|
{
|
|
if (sub == null || sub == resolved) continue;
|
|
prop.objectReferenceValue = sub;
|
|
if (prop.objectReferenceValue != null)
|
|
{
|
|
match = sub;
|
|
matchCount++;
|
|
if (matchCount > 1) break;
|
|
}
|
|
}
|
|
|
|
if (matchCount == 1)
|
|
{
|
|
prop.objectReferenceValue = match;
|
|
return true;
|
|
}
|
|
|
|
// Clean up: probing may have left the property dirty
|
|
prop.objectReferenceValue = null;
|
|
|
|
if (matchCount > 1)
|
|
{
|
|
error = $"Multiple compatible sub-assets found in '{subAssetPath}'. " +
|
|
"Use {\"guid\": \"...\", \"spriteName\": \"<name>\"} or " +
|
|
"{\"guid\": \"...\", \"fileID\": <id>} for precise selection.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If the resolved object is a GameObject but the property expects a Component,
|
|
// try each component on the GameObject until one is accepted.
|
|
if (resolved is GameObject go)
|
|
{
|
|
var components = go.GetComponents<Component>();
|
|
foreach (var comp in components)
|
|
{
|
|
if (comp == null) continue;
|
|
prop.objectReferenceValue = comp;
|
|
if (prop.objectReferenceValue != null)
|
|
return true;
|
|
}
|
|
error = $"GameObject '{go.name}' found but no compatible component for the property type.";
|
|
return false;
|
|
}
|
|
|
|
error = $"Object '{resolved.name}' (type: {resolved.GetType().Name}) is not compatible with the property type.";
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a scene GameObject by name and assigns it (or a component on it)
|
|
/// to a SerializedProperty. Uses GameObjectLookup for robust search
|
|
/// including inactive objects and prefab stage support.
|
|
/// </summary>
|
|
private static bool ResolveSceneObjectByName(SerializedProperty prop, string name, string componentFilter, out string error)
|
|
{
|
|
error = null;
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
error = "Cannot resolve object reference from empty name.";
|
|
return false;
|
|
}
|
|
|
|
var ids = GameObjectLookup.SearchGameObjects(
|
|
GameObjectLookup.SearchMethod.ByName, name, includeInactive: true, maxResults: 1);
|
|
|
|
if (ids.Count == 0)
|
|
{
|
|
error = $"No GameObject named '{name}' found in scene.";
|
|
return false;
|
|
}
|
|
|
|
var go = GameObjectLookup.FindById(ids[0]);
|
|
if (go == null)
|
|
{
|
|
error = $"GameObject '{name}' found but could not be resolved.";
|
|
return false;
|
|
}
|
|
|
|
return AssignObjectReference(prop, go, componentFilter, out error);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a child SerializedProperty by name, falling back to underscore-insensitive matching.
|
|
/// The batch_execute transport can strip underscores from JSON keys
|
|
/// (e.g. m_PersistentCalls → mPersistentCalls), so we iterate immediate children
|
|
/// and compare with underscores removed.
|
|
/// </summary>
|
|
private static SerializedProperty FindPropertyRelativeFuzzy(SerializedProperty parent, string key)
|
|
{
|
|
var child = parent.FindPropertyRelative(key);
|
|
if (child != null) return child;
|
|
|
|
string normalizedKey = key.Replace("_", "").ToLowerInvariant();
|
|
|
|
var end = parent.GetEndProperty();
|
|
var iter = parent.Copy();
|
|
if (!iter.Next(true)) return null;
|
|
|
|
while (!SerializedProperty.EqualContents(iter, end))
|
|
{
|
|
if (iter.depth == parent.depth + 1)
|
|
{
|
|
string normalizedName = iter.name.Replace("_", "").ToLowerInvariant();
|
|
if (normalizedName == normalizedKey)
|
|
return parent.FindPropertyRelative(iter.name);
|
|
}
|
|
if (!iter.Next(false))
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool SetEnum(SerializedProperty prop, JToken value, out string error)
|
|
{
|
|
error = null;
|
|
var names = prop.enumNames;
|
|
if (names == null || names.Length == 0)
|
|
{
|
|
error = "Enum has no names.";
|
|
return false;
|
|
}
|
|
|
|
if (value.Type == JTokenType.Integer)
|
|
{
|
|
int idx = value.Value<int>();
|
|
if (idx < 0 || idx >= names.Length)
|
|
{
|
|
error = $"Enum index out of range: {idx}.";
|
|
return false;
|
|
}
|
|
prop.enumValueIndex = idx;
|
|
return true;
|
|
}
|
|
|
|
string s = value.ToString();
|
|
for (int i = 0; i < names.Length; i++)
|
|
{
|
|
if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
prop.enumValueIndex = i;
|
|
return true;
|
|
}
|
|
}
|
|
error = $"Unknown enum name '{s}'.";
|
|
return false;
|
|
}
|
|
|
|
private static long GetSpriteFileId(Sprite sprite)
|
|
{
|
|
if (sprite == null)
|
|
return 0;
|
|
|
|
try
|
|
{
|
|
var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
|
|
return unchecked((long)globalId.targetObjectId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
McpLog.Warn($"Failed to get fileID for sprite '{sprite.name}' (instanceID={sprite.GetInstanceIDCompat()}): {ex.Message}");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private static bool IsHexString(string str)
|
|
{
|
|
foreach (char c in str)
|
|
{
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|