Fix manage_components set_property for object references (#551)

main
dsarno 2026-01-13 22:23:18 -08:00 committed by GitHub
parent f4a8b05094
commit b874922cb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 166 additions and 29 deletions

View File

@ -197,11 +197,12 @@ namespace MCPForUnity.Editor.Helpers
}
}
// Try non-public serialized fields - check both original and normalized names
BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
fieldInfo = type.GetField(propertyName, privateFlags)
?? type.GetField(normalizedName, privateFlags);
if (fieldInfo != null && fieldInfo.GetCustomAttribute<SerializeField>() != null)
// 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
fieldInfo = FindSerializedFieldInHierarchy(type, propertyName)
?? FindSerializedFieldInHierarchy(type, normalizedName);
if (fieldInfo != null)
{
try
{
@ -252,14 +253,23 @@ namespace MCPForUnity.Editor.Helpers
}
}
// Include private [SerializeField] fields
foreach (var field in componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
// 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))
{
if (field.GetCustomAttribute<SerializeField>() != null)
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;
@ -267,6 +277,37 @@ namespace MCPForUnity.Editor.Helpers
// --- 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>
private 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 =

View File

@ -318,38 +318,134 @@ namespace MCPForUnity.Runtime.Serialization
#if UNITY_EDITOR
if (reader.TokenType == JsonToken.String)
{
string strValue = reader.Value.ToString();
// Check if it looks like a GUID (32 hex chars, optionally with hyphens)
if (IsValidGuid(strValue))
{
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(strValue.Replace("-", "").ToLowerInvariant());
if (!string.IsNullOrEmpty(path))
{
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
if (asset != null) return asset;
}
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{strValue}' as type '{objectType.Name}'.");
return null;
}
// Assume it's an asset path
string path = reader.Value.ToString();
return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
var loadedAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(strValue, objectType);
if (loadedAsset == null)
{
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{strValue}' as type '{objectType.Name}'.");
}
return loadedAsset;
}
if (reader.TokenType == JsonToken.StartObject)
{
JObject jo = JObject.Load(reader);
// Try to resolve by GUID first (for assets like ScriptableObjects, Materials, etc.)
if (jo.TryGetValue("guid", out JToken guidToken) && guidToken.Type == JTokenType.String)
{
string guid = guidToken.ToString().Replace("-", "").ToLowerInvariant();
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path))
{
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
if (asset != null) return asset;
}
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{guidToken}' as type '{objectType.Name}'.");
return null;
}
// Try to resolve by instanceID
if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
{
int instanceId = idToken.ToObject<int>();
UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
if (obj != null)
{
// Direct type match
if (objectType.IsAssignableFrom(obj.GetType()))
{
return obj;
}
// Special case: expecting Transform but got GameObject - get its transform
if (objectType == typeof(Transform) && obj is GameObject go)
{
return go.transform;
}
// Could potentially try finding by name as a fallback if ID lookup fails/isn't present
// but that's less reliable.
// Special case: expecting a Component type but got GameObject - try to get the component
if (typeof(Component).IsAssignableFrom(objectType) && obj is GameObject gameObj)
{
var component = gameObj.GetComponent(objectType);
if (component != null)
{
return component;
}
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] GameObject '{gameObj.name}' (ID: {instanceId}) does not have a '{objectType.Name}' component.");
return null;
}
// Type mismatch with no automatic conversion available
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Instance ID {instanceId} resolved to '{obj.GetType().Name}' but expected '{objectType.Name}'.");
return null;
}
// Instance ID lookup failed - this can happen if the object was destroyed or ID is stale
string objectName = jo.TryGetValue("name", out JToken nameToken) ? nameToken.ToString() : "unknown";
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not resolve instance ID {instanceId} (name: '{objectName}') to a valid {objectType.Name}. The object may have been destroyed or the ID is stale.");
return null;
}
// Check if there's an asset path in the object
if (jo.TryGetValue("path", out JToken pathToken) && pathToken.Type == JTokenType.String)
{
string path = pathToken.ToString();
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
if (asset != null)
{
return asset;
}
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{path}' as type '{objectType.Name}'.");
return null;
}
// Object format not recognized
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] JSON object missing 'instanceID', 'guid', or 'path' field for {objectType.Name} deserialization. Object: {jo.ToString(Formatting.None)}");
return null;
}
// Unexpected token type
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Unexpected token type '{reader.TokenType}' when deserializing {objectType.Name}. Expected Null, String, or Object.");
return null;
#else
// Runtime deserialization is tricky without AssetDatabase/EditorUtility
// Maybe log a warning and return null or existingValue?
UnityEngine.Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
// Skip the token to avoid breaking the reader
if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
else if (reader.TokenType == JsonToken.String) reader.ReadAsString();
// Return null or existing value, depending on desired behavior
// Skip the current token to avoid breaking the reader state
reader.Skip();
// Return existing value since we can't deserialize without Editor APIs
return existingValue;
#endif
}
throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
/// <summary>
/// Checks if a string looks like a valid GUID (32 hex chars, with or without hyphens).
/// </summary>
private static bool IsValidGuid(string str)
{
if (string.IsNullOrEmpty(str)) return false;
string normalized = str.Replace("-", "");
if (normalized.Length != 32) return false;
foreach (char c in normalized)
{
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return true;
}
}
}