Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions MCPForUnity/Editor/Helpers/ComponentOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,37 @@ public static bool SetProperty(Component component, string propertyName, JToken
return SetViaSerializedProperty(component, propertyName, normalizedName, value, out error);
}

// Try property first - check both original and normalized names for backwards compatibility
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
// 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;

// Try property first
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
try
{
object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType);
// Detect conversion failure: null result when input wasn't null
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'.";
Expand All @@ -198,15 +220,14 @@ public static bool SetProperty(Component component, string propertyName, JToken
}
}

// Try field - check both original and normalized names for backwards compatibility
FieldInfo fieldInfo = type.GetField(propertyName, flags)
// Try field
FieldInfo fieldInfo = type.GetField(propertyName, flags)
?? type.GetField(normalizedName, flags);
if (fieldInfo != null && !fieldInfo.IsInitOnly)
{
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
// Detect conversion failure: null result when input wasn't null
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
Expand All @@ -222,17 +243,14 @@ public static bool SetProperty(Component component, string propertyName, JToken
}
}

// Try non-public serialized fields - traverse inheritance hierarchy
// Type.GetField() with NonPublic only finds fields declared directly on that type,
// so we need to walk up the inheritance chain manually
// Try non-public serialized fields — traverse inheritance hierarchy
fieldInfo = FindSerializedFieldInHierarchy(type, propertyName)
?? FindSerializedFieldInHierarchy(type, normalizedName);
if (fieldInfo != null)
{
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
// Detect conversion failure: null result when input wasn't null
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
Expand Down Expand Up @@ -307,7 +325,7 @@ public static List<string> GetAccessibleMembers(Type componentType)
/// 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>
private static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName)
internal static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName)
{
if (type == null || string.IsNullOrEmpty(fieldName))
return null;
Expand Down
161 changes: 78 additions & 83 deletions MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,32 @@ internal static object SetComponentPropertiesInternal(GameObject targetGo, strin

try
{
bool setResult = SetProperty(targetComponent, propName, propValue);
bool setResult;
string setError;

// Nested paths (e.g. "transform.position") need local handling
// since ComponentOps doesn't support dot/bracket notation.
if (propName.Contains('.') || propName.Contains('['))
{
setResult = SetNestedProperty(targetComponent, propName, propValue, InputSerializer, out setError);
}
else
{
// ComponentOps handles reflection + SerializedProperty fallback
setResult = ComponentOps.SetProperty(targetComponent, propName, propValue, out setError);
}

if (!setResult)
{
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(propName, availableProperties);
var msg = suggestions.Any()
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
string msg = setError;
if (msg == null || msg.Contains("not found"))
{
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(propName, availableProperties);
msg = suggestions.Any()
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
}
McpLog.Warn($"[ManageGameObject] {msg}");
failures.Add(msg);
}
Expand All @@ -199,73 +217,17 @@ internal static object SetComponentPropertiesInternal(GameObject targetGo, strin

private static JsonSerializer InputSerializer => UnityJsonSerializer.Instance;

private static bool SetProperty(object target, string memberName, JToken value)
{
Type type = target.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;

string normalizedName = Helpers.ParamCoercion.NormalizePropertyName(memberName);
var inputSerializer = InputSerializer;

try
{
if (memberName.Contains('.') || memberName.Contains('['))
{
return SetNestedProperty(target, memberName, value, inputSerializer);
}

PropertyInfo propInfo = type.GetProperty(memberName, flags) ?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
propInfo.SetValue(target, convertedValue);
return true;
}
}
else
{
FieldInfo fieldInfo = type.GetField(memberName, flags) ?? type.GetField(normalizedName, flags);
if (fieldInfo != null)
{
object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
fieldInfo.SetValue(target, convertedValue);
return true;
}
}
else
{
var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase)
?? type.GetField(normalizedName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (npField != null && npField.GetCustomAttribute<SerializeField>() != null)
{
object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
npField.SetValue(target, convertedValue);
return true;
}
}
}
}
}
catch (Exception ex)
{
McpLog.Error($"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}");
}
return false;
}

private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer)
private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer, out string error)
{
error = null;
try
{
string[] pathParts = SplitPropertyPath(path);
if (pathParts.Length == 0)
{
error = $"Invalid nested property path '{path}'.";
return false;
}

object currentObject = target;
Type currentType = currentObject.GetType();
Expand Down Expand Up @@ -299,15 +261,15 @@ private static bool SetNestedProperty(object target, string path, JToken value,
fieldInfo = currentType.GetField(part, flags);
if (fieldInfo == null)
{
McpLog.Warn($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'");
error = $"Could not find property or field '{part}' on type '{currentType.Name}' in path '{path}'.";
return false;
}
}

currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject);
if (currentObject == null)
{
McpLog.Warn($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties.");
error = $"Property '{part}' is null in path '{path}', cannot access nested properties.";
return false;
}

Expand All @@ -316,26 +278,36 @@ private static bool SetNestedProperty(object target, string path, JToken value,
if (currentObject is Material[])
{
var materials = currentObject as Material[];
if (materials.Length == 0)
{
error = $"Material array is empty in path '{path}', cannot access index {arrayIndex}.";
return false;
}
if (arrayIndex < 0 || arrayIndex >= materials.Length)
{
McpLog.Warn($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})");
error = $"Material index {arrayIndex} out of range (0-{materials.Length - 1}) in path '{path}'.";
return false;
}
currentObject = materials[arrayIndex];
}
else if (currentObject is System.Collections.IList)
{
var list = currentObject as System.Collections.IList;
if (list.Count == 0)
{
error = $"List is empty in path '{path}', cannot access index {arrayIndex}.";
return false;
}
if (arrayIndex < 0 || arrayIndex >= list.Count)
{
McpLog.Warn($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})");
error = $"Index {arrayIndex} out of range (0-{list.Count - 1}) in path '{path}'.";
return false;
}
currentObject = list[arrayIndex];
}
else
{
McpLog.Warn($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index.");
error = $"Property '{part}' is not an array or list in path '{path}', cannot access by index.";
return false;
}
}
Expand All @@ -347,7 +319,12 @@ private static bool SetNestedProperty(object target, string path, JToken value,

if (currentObject is Material material && finalPart.StartsWith("_"))
{
return MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer);
if (!MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer))
{
error = $"Failed to set shader property '{finalPart}' on material '{material.name}' in path '{path}'.";
return false;
}
return true;
}

PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags);
Expand All @@ -359,24 +336,42 @@ private static bool SetNestedProperty(object target, string path, JToken value,
finalPropInfo.SetValue(currentObject, convertedValue);
return true;
}
error = $"Failed to convert value for '{finalPart}' to type '{finalPropInfo.PropertyType.Name}' in path '{path}'.";
return false;
}
else

FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
if (finalFieldInfo != null)
{
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
if (finalFieldInfo != null)
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
finalFieldInfo.SetValue(currentObject, convertedValue);
return true;
}
finalFieldInfo.SetValue(currentObject, convertedValue);
return true;
}
error = $"Failed to convert value for '{finalPart}' to type '{finalFieldInfo.FieldType.Name}' in path '{path}'.";
return false;
}

// Try non-public [SerializeField] fields (nested paths need this too)
FieldInfo serializedField = ComponentOps.FindSerializedFieldInHierarchy(currentType, finalPart);
if (serializedField != null)
{
object convertedValue = ConvertJTokenToType(value, serializedField.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
serializedField.SetValue(currentObject, convertedValue);
return true;
}
error = $"Failed to convert value for '{finalPart}' to type '{serializedField.FieldType.Name}' in path '{path}'.";
return false;
}

error = $"Property or field '{finalPart}' not found on type '{currentType.Name}' in path '{path}'.";
}
catch (Exception ex)
{
McpLog.Error($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}");
error = $"Error setting nested property '{path}': {ex.Message}";
}

return false;
Expand Down
Loading