From 4d7cfade9ba7749e89895d544f209d4a57f4e36e Mon Sep 17 00:00:00 2001 From: Charlie Hess Date: Wed, 4 Feb 2026 15:45:33 -0500 Subject: [PATCH] fix: enable private field access in UpdateComponentTool Use SerializedObject API as primary method for modifying component fields, with enhanced reflection as fallback. This fixes three issues: 1. Private fields in base classes now accessible (recursive type search) 2. Asset references can be set by path or GUID 3. Nested field paths supported via SerializedObject.FindProperty() Co-authored-by: Cursor --- Editor/Tools/UpdateComponentTool.cs | 490 +++++++++++++++++++++++++++- 1 file changed, 476 insertions(+), 14 deletions(-) diff --git a/Editor/Tools/UpdateComponentTool.cs b/Editor/Tools/UpdateComponentTool.cs index 390dce0b..e86e29c9 100644 --- a/Editor/Tools/UpdateComponentTool.cs +++ b/Editor/Tools/UpdateComponentTool.cs @@ -238,13 +238,54 @@ private Type FindComponentType(string componentName) return null; } + + /// + /// Recursively search for a field through the type hierarchy + /// + /// The type to start searching from + /// The name of the field to find + /// The FieldInfo if found, null otherwise + private FieldInfo GetFieldRecursive(Type type, string fieldName) + { + while (type != null && type != typeof(object)) + { + FieldInfo field = type.GetField(fieldName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (field != null) + return field; + type = type.BaseType; + } + return null; + } + + /// + /// Recursively search for a property through the type hierarchy + /// + /// The type to start searching from + /// The name of the property to find + /// The PropertyInfo if found, null otherwise + private PropertyInfo GetPropertyRecursive(Type type, string propertyName) + { + while (type != null && type != typeof(object)) + { + PropertyInfo prop = type.GetProperty(propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (prop != null) + return prop; + type = type.BaseType; + } + return null; + } /// /// Update component data based on the provided JObject + /// Uses SerializedObject API as primary method (handles base class fields and nested paths), + /// with reflection as fallback for non-serialized properties. /// /// The component to update /// The data to apply to the component - /// True if the component was updated successfully + /// Error message if any fields failed to update + /// True if all fields were updated successfully private bool UpdateComponentData(Component component, JObject componentData, out string errorMessage) { errorMessage = ""; @@ -261,21 +302,36 @@ private bool UpdateComponentData(Component component, JObject componentData, out // Record object for undo Undo.RecordObject(component, $"Update {componentType.Name} fields"); + // Create SerializedObject for the primary approach + SerializedObject serializedObject = new SerializedObject(component); + // Process each field or property in the component data foreach (var property in componentData.Properties()) { string fieldName = property.Name; JToken fieldValue = property.Value; - // Skip null values - if (string.IsNullOrEmpty(fieldName) || fieldValue.Type == JTokenType.Null) + // Skip null field names + if (string.IsNullOrEmpty(fieldName)) + { + continue; + } + + // Primary approach: Try SerializedObject API first + // This handles base class fields and nested paths automatically + SerializedProperty serializedProperty = serializedObject.FindProperty(fieldName); + + if (serializedProperty != null) { + SetSerializedPropertyValue(serializedProperty, fieldValue); continue; } - // Try to update field - FieldInfo fieldInfo = componentType.GetField(fieldName, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + // Fallback: Use reflection with recursive base class search + // This handles non-serialized properties that SerializedObject can't access + + // Try to find field (including in base classes) + FieldInfo fieldInfo = GetFieldRecursive(componentType, fieldName); if (fieldInfo != null) { @@ -284,11 +340,10 @@ private bool UpdateComponentData(Component component, JObject componentData, out continue; } - // Try to update property if not found as a field - PropertyInfo propertyInfo = componentType.GetProperty(fieldName, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + // Try to find property (including in base classes) + PropertyInfo propertyInfo = GetPropertyRecursive(componentType, fieldName); - if (propertyInfo != null) + if (propertyInfo != null && propertyInfo.CanWrite) { object value = ConvertJTokenToValue(fieldValue, propertyInfo.PropertyType); propertyInfo.SetValue(component, value); @@ -296,12 +351,352 @@ private bool UpdateComponentData(Component component, JObject componentData, out } fullSuccess = false; - errorMessage = $"Field or Property with name '{fieldName}' not found on component '{componentType.Name}'"; + errorMessage = $"Field or Property with name '{fieldName}' not found on component '{componentType.Name}'"; + McpLogger.LogWarning($"[MCP Unity] {errorMessage}"); } + + // Apply all SerializedObject changes + serializedObject.ApplyModifiedProperties(); return fullSuccess; } + /// + /// Update component data using Unity's SerializedObject API + /// This properly handles all serialized fields including private ones in base classes + /// + /// The component to update + /// The data to apply to the component + /// Error message if any fields failed to update + /// True if all fields were updated successfully + private bool UpdateComponentDataSerialized(Component component, JObject componentData, out string errorMessage) + { + errorMessage = ""; + SerializedObject serializedObject = new SerializedObject(component); + bool allFieldsFound = true; + + foreach (var property in componentData.Properties()) + { + string fieldPath = property.Name; + JToken fieldValue = property.Value; + + // Skip null values + if (string.IsNullOrEmpty(fieldPath) || fieldValue.Type == JTokenType.Null) + { + continue; + } + + // SerializedObject.FindProperty uses Unity's serialization path + SerializedProperty serializedProperty = serializedObject.FindProperty(fieldPath); + + if (serializedProperty == null) + { + McpLogger.LogWarning($"[MCP Unity] SerializedProperty '{fieldPath}' not found on {component.GetType().Name}"); + errorMessage = $"Property '{fieldPath}' not found"; + allFieldsFound = false; + continue; + } + + SetSerializedPropertyValue(serializedProperty, fieldValue); + } + + serializedObject.ApplyModifiedProperties(); + return allFieldsFound; + } + + /// + /// Set a SerializedProperty value from a JToken + /// + /// The SerializedProperty to set + /// The JToken value to apply + private void SetSerializedPropertyValue(SerializedProperty prop, JToken value) + { + switch (prop.propertyType) + { + case SerializedPropertyType.Integer: + prop.intValue = value.ToObject(); + break; + + case SerializedPropertyType.Boolean: + prop.boolValue = value.ToObject(); + break; + + case SerializedPropertyType.Float: + prop.floatValue = value.ToObject(); + break; + + case SerializedPropertyType.String: + prop.stringValue = value.ToObject(); + break; + + case SerializedPropertyType.Color: + if (value.Type == JTokenType.Object) + { + JObject color = (JObject)value; + prop.colorValue = new Color( + color["r"]?.ToObject() ?? 0f, + color["g"]?.ToObject() ?? 0f, + color["b"]?.ToObject() ?? 0f, + color["a"]?.ToObject() ?? 1f + ); + } + break; + + case SerializedPropertyType.Vector2: + if (value.Type == JTokenType.Object) + { + JObject v2 = (JObject)value; + prop.vector2Value = new Vector2( + v2["x"]?.ToObject() ?? 0f, + v2["y"]?.ToObject() ?? 0f + ); + } + break; + + case SerializedPropertyType.Vector3: + if (value.Type == JTokenType.Object) + { + JObject v3 = (JObject)value; + prop.vector3Value = new Vector3( + v3["x"]?.ToObject() ?? 0f, + v3["y"]?.ToObject() ?? 0f, + v3["z"]?.ToObject() ?? 0f + ); + } + break; + + case SerializedPropertyType.Vector4: + if (value.Type == JTokenType.Object) + { + JObject v4 = (JObject)value; + prop.vector4Value = new Vector4( + v4["x"]?.ToObject() ?? 0f, + v4["y"]?.ToObject() ?? 0f, + v4["z"]?.ToObject() ?? 0f, + v4["w"]?.ToObject() ?? 0f + ); + } + break; + + case SerializedPropertyType.Quaternion: + if (value.Type == JTokenType.Object) + { + JObject q = (JObject)value; + prop.quaternionValue = new Quaternion( + q["x"]?.ToObject() ?? 0f, + q["y"]?.ToObject() ?? 0f, + q["z"]?.ToObject() ?? 0f, + q["w"]?.ToObject() ?? 1f + ); + } + break; + + case SerializedPropertyType.Rect: + if (value.Type == JTokenType.Object) + { + JObject rect = (JObject)value; + prop.rectValue = new Rect( + rect["x"]?.ToObject() ?? 0f, + rect["y"]?.ToObject() ?? 0f, + rect["width"]?.ToObject() ?? 0f, + rect["height"]?.ToObject() ?? 0f + ); + } + break; + + case SerializedPropertyType.Bounds: + if (value.Type == JTokenType.Object) + { + JObject bounds = (JObject)value; + JObject center = bounds["center"] as JObject; + JObject size = bounds["size"] as JObject; + prop.boundsValue = new Bounds( + center != null ? new Vector3( + center["x"]?.ToObject() ?? 0f, + center["y"]?.ToObject() ?? 0f, + center["z"]?.ToObject() ?? 0f + ) : Vector3.zero, + size != null ? new Vector3( + size["x"]?.ToObject() ?? 0f, + size["y"]?.ToObject() ?? 0f, + size["z"]?.ToObject() ?? 0f + ) : Vector3.one + ); + } + break; + + case SerializedPropertyType.Enum: + if (value.Type == JTokenType.String) + { + // Find enum value by name + string[] enumNames = prop.enumNames; + string enumValue = value.ToObject(); + int index = Array.IndexOf(enumNames, enumValue); + if (index >= 0) + { + prop.enumValueIndex = index; + } + else + { + // Try case-insensitive match + for (int i = 0; i < enumNames.Length; i++) + { + if (string.Equals(enumNames[i], enumValue, StringComparison.OrdinalIgnoreCase)) + { + prop.enumValueIndex = i; + break; + } + } + } + } + else if (value.Type == JTokenType.Integer) + { + prop.enumValueIndex = value.ToObject(); + } + break; + + case SerializedPropertyType.ObjectReference: + // Handle asset references + if (value.Type == JTokenType.String) + { + string assetPath = value.ToObject(); + if (!string.IsNullOrEmpty(assetPath)) + { + UnityEngine.Object asset = null; + + // Try to load as asset path + if (assetPath.StartsWith("Assets/") || assetPath.StartsWith("Packages/")) + { + asset = AssetDatabase.LoadAssetAtPath(assetPath); + } + + // If not found, try to find by name + if (asset == null) + { + string[] guids = AssetDatabase.FindAssets(assetPath); + if (guids.Length > 0) + { + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + asset = AssetDatabase.LoadAssetAtPath(path); + } + } + + prop.objectReferenceValue = asset; + } + else + { + prop.objectReferenceValue = null; + } + } + else if (value.Type == JTokenType.Object) + { + JObject obj = (JObject)value; + string guid = obj["guid"]?.ToObject(); + string path = obj["path"]?.ToObject(); + + if (!string.IsNullOrEmpty(guid)) + { + path = AssetDatabase.GUIDToAssetPath(guid); + } + + if (!string.IsNullOrEmpty(path)) + { + prop.objectReferenceValue = AssetDatabase.LoadAssetAtPath(path); + } + } + else if (value.Type == JTokenType.Null) + { + prop.objectReferenceValue = null; + } + break; + + case SerializedPropertyType.LayerMask: + prop.intValue = value.ToObject(); + break; + + case SerializedPropertyType.Vector2Int: + if (value.Type == JTokenType.Object) + { + JObject v2i = (JObject)value; + prop.vector2IntValue = new Vector2Int( + v2i["x"]?.ToObject() ?? 0, + v2i["y"]?.ToObject() ?? 0 + ); + } + break; + + case SerializedPropertyType.Vector3Int: + if (value.Type == JTokenType.Object) + { + JObject v3i = (JObject)value; + prop.vector3IntValue = new Vector3Int( + v3i["x"]?.ToObject() ?? 0, + v3i["y"]?.ToObject() ?? 0, + v3i["z"]?.ToObject() ?? 0 + ); + } + break; + + case SerializedPropertyType.RectInt: + if (value.Type == JTokenType.Object) + { + JObject rectInt = (JObject)value; + prop.rectIntValue = new RectInt( + rectInt["x"]?.ToObject() ?? 0, + rectInt["y"]?.ToObject() ?? 0, + rectInt["width"]?.ToObject() ?? 0, + rectInt["height"]?.ToObject() ?? 0 + ); + } + break; + + case SerializedPropertyType.BoundsInt: + if (value.Type == JTokenType.Object) + { + JObject boundsInt = (JObject)value; + JObject position = boundsInt["position"] as JObject; + JObject size = boundsInt["size"] as JObject; + prop.boundsIntValue = new BoundsInt( + position != null ? new Vector3Int( + position["x"]?.ToObject() ?? 0, + position["y"]?.ToObject() ?? 0, + position["z"]?.ToObject() ?? 0 + ) : Vector3Int.zero, + size != null ? new Vector3Int( + size["x"]?.ToObject() ?? 0, + size["y"]?.ToObject() ?? 0, + size["z"]?.ToObject() ?? 0 + ) : Vector3Int.one + ); + } + break; + + case SerializedPropertyType.Generic: + // For complex types, try to iterate children + if (value.Type == JTokenType.Object) + { + JObject obj = (JObject)value; + foreach (var child in obj.Properties()) + { + SerializedProperty childProp = prop.FindPropertyRelative(child.Name); + if (childProp != null) + { + SetSerializedPropertyValue(childProp, child.Value); + } + } + } + break; + + case SerializedPropertyType.ArraySize: + prop.intValue = value.ToObject(); + break; + + default: + McpLogger.LogWarning($"[MCP Unity] Unsupported property type: {prop.propertyType} for {prop.name}"); + break; + } + } + /// /// Convert a JToken to a value of the specified type /// @@ -387,10 +782,77 @@ private object ConvertJTokenToValue(JToken token, Type targetType) ); } - // Handle UnityEngine.Object types; - if (targetType == typeof(UnityEngine.Object)) + // Handle UnityEngine.Object types (assets) by path or GUID + if (typeof(UnityEngine.Object).IsAssignableFrom(targetType)) { - return token.ToObject(); + if (token.Type == JTokenType.String) + { + string assetPath = token.ToObject(); + + if (string.IsNullOrEmpty(assetPath)) + { + return null; + } + + // Try to load as asset path + if (assetPath.StartsWith("Assets/") || assetPath.StartsWith("Packages/")) + { + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); + if (asset != null) + { + return asset; + } + McpLogger.LogWarning($"[MCP Unity] Could not load asset at path: {assetPath}"); + } + + // Try to find by name in project + string[] guids = AssetDatabase.FindAssets($"{assetPath} t:{targetType.Name}"); + if (guids.Length > 0) + { + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + return AssetDatabase.LoadAssetAtPath(path, targetType); + } + + // Final fallback - search without type filter + guids = AssetDatabase.FindAssets(assetPath); + if (guids.Length > 0) + { + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(path, targetType); + if (asset != null) + { + return asset; + } + } + + McpLogger.LogWarning($"[MCP Unity] Could not find asset: {assetPath}"); + return null; + } + else if (token.Type == JTokenType.Object) + { + // Handle object with guid or path property + JObject obj = (JObject)token; + string guid = obj["guid"]?.ToObject(); + string path = obj["path"]?.ToObject(); + + if (!string.IsNullOrEmpty(guid)) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + if (!string.IsNullOrEmpty(assetPath)) + { + return AssetDatabase.LoadAssetAtPath(assetPath, targetType); + } + McpLogger.LogWarning($"[MCP Unity] Could not find asset with GUID: {guid}"); + } + else if (!string.IsNullOrEmpty(path)) + { + return AssetDatabase.LoadAssetAtPath(path, targetType); + } + } + else if (token.Type == JTokenType.Null) + { + return null; + } } // Handle enum types