Files
coplaydev--unity-mcp/MCPForUnity/Editor/Helpers/ComponentOps.cs
T
2026-07-13 12:49:17 +08:00

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;
}
}
}