From 9b112243578d9a9c9a1b147b4b16efb992424fbf Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 9 Apr 2025 19:58:42 -0700 Subject: [PATCH 1/7] Feat: Generalize GetComponentData using reflection --- .../Editor/Tools/ManageGameObject.cs | 139 +++++++++++++++--- 1 file changed, 119 insertions(+), 20 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 414603da..691ade61 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -2182,39 +2182,138 @@ private static object GetGameObjectData(GameObject go) } /// - /// Creates a serializable representation of a Component. - /// TODO: Add property serialization. + /// Creates a serializable representation of a Component, attempting to serialize + /// public properties and fields using reflection. /// private static object GetComponentData(Component c) { - if (c == null) - return null; + if (c == null) return null; + var data = new Dictionary { { "typeName", c.GetType().FullName }, - { "instanceID", c.GetInstanceID() }, + { "instanceID", c.GetInstanceID() } }; - // Attempt to serialize public properties/fields (can be noisy/complex) - /* - try { - var properties = new Dictionary(); - var type = c.GetType(); - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; - - foreach (var prop in type.GetProperties(flags).Where(p => p.CanRead && p.GetIndexParameters().Length == 0)) { - try { properties[prop.Name] = prop.GetValue(c); } catch { } + var serializableProperties = new Dictionary(); + Type componentType = c.GetType(); + // Include NonPublic flags for fields, keep Public for properties initially + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; + + // Process Properties (Still only public for properties) + // Using propFlags here + foreach (var propInfo in componentType.GetProperties(propFlags)) + { + // Skip indexers and write-only properties, and skip the transform property as it's handled by GetGameObjectData + if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + + try + { + object value = propInfo.GetValue(c); + string propName = propInfo.Name; + Type propType = propInfo.PropertyType; + + AddSerializableValue(serializableProperties, propName, propType, value); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not read property {propInfo.Name} on {componentType.Name}: {ex.Message}"); + } + } + + // Process Fields (Include NonPublic) + // Using fieldFlags here + foreach (var fieldInfo in componentType.GetFields(fieldFlags)) + { + // Skip backing fields for properties (common pattern) + if (fieldInfo.Name.EndsWith("k__BackingField")) continue; + + // Only include public fields or non-public fields with [SerializeField] + // Check if the field is explicitly marked with SerializeField or if it's public + bool isSerializable = fieldInfo.IsPublic || fieldInfo.IsDefined(typeof(SerializeField), inherit: false); // inherit: false is typical for SerializeField + + if (!isSerializable) continue; // Skip if not public and not explicitly serialized + + try + { + object value = fieldInfo.GetValue(c); + string fieldName = fieldInfo.Name; + Type fieldType = fieldInfo.FieldType; + + AddSerializableValue(serializableProperties, fieldName, fieldType, value); } - foreach (var field in type.GetFields(flags)) { - try { properties[field.Name] = field.GetValue(c); } catch { } + catch (Exception ex) + { + Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); } - data["properties"] = properties; - } catch (Exception ex) { - data["propertiesError"] = ex.Message; } - */ + + if (serializableProperties.Count > 0) + { + data["properties"] = serializableProperties; // Add the collected properties + } + return data; } + + // Helper function to decide how to serialize different types + private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) + { + if (value == null) + { + dict[name] = null; + return; + } + + // Primitives & Enums + if (type.IsPrimitive || type.IsEnum || type == typeof(string)) + { + dict[name] = value; + } + // Known Unity Structs (add more as needed: Rect, Bounds, etc.) + else if (type == typeof(Vector2)) { var v = (Vector2)value; dict[name] = new { v.x, v.y }; } + else if (type == typeof(Vector3)) { var v = (Vector3)value; dict[name] = new { v.x, v.y, v.z }; } + else if (type == typeof(Vector4)) { var v = (Vector4)value; dict[name] = new { v.x, v.y, v.z, v.w }; } + else if (type == typeof(Quaternion)) { var q = (Quaternion)value; dict[name] = new { x = q.eulerAngles.x, y = q.eulerAngles.y, z = q.eulerAngles.z }; } // Serialize as Euler angles for readability + else if (type == typeof(Color)) { var c = (Color)value; dict[name] = new { c.r, c.g, c.b, c.a }; } + // UnityEngine.Object References + else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) + { + var obj = value as UnityEngine.Object; + if (obj != null) { + // Use dynamic or a helper class for flexible properties if adding assetPath + var refData = new Dictionary { + { "name", obj.name }, + { "instanceID", obj.GetInstanceID() }, + { "typeName", obj.GetType().FullName } + }; + string assetPath = AssetDatabase.GetAssetPath(obj); + if (!string.IsNullOrEmpty(assetPath)) { + refData["assetPath"] = assetPath; + } + dict[name] = refData; + + } else { + dict[name] = null; // The object reference is null + } + } + // Add handling for basic Lists/Arrays of primitives? (Example for List) + else if (type == typeof(List)) { + dict[name] = value as List; // Directly serializable + } + else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { + // Could attempt to serialize lists of primitives/structs/references here if needed + dict[name] = $"[Skipped List<{type.GetGenericArguments()[0].Name}>]"; + } + else if (type.IsArray) { + dict[name] = $"[Skipped Array<{type.GetElementType().Name}>]"; + } + // Skip other complex types for now + else { + dict[name] = $"[Skipped complex type: {type.FullName}]"; + } + } } } From 15ba68f47319543ac656b82cb2af2d0910f7a22d Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 08:12:20 -0700 Subject: [PATCH 2/7] feat: Improve GameObject serialization and add includeNonPublicSerialized flag --- .../Editor/Tools/ManageGameObject.cs | 255 ++++++++++++++---- UnityMcpBridge/Editor/UnityMcpBridge.cs | 1 + UnityMcpServer/src/tools/manage_gameobject.py | 5 +- UnityMcpServer/src/uv.lock | 4 +- 4 files changed, 214 insertions(+), 51 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 691ade61..75c4a62b 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Newtonsoft.Json; // Added for JsonSerializationException using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; @@ -21,6 +22,11 @@ public static class ManageGameObject public static object HandleCommand(JObject @params) { + // --- DEBUG --- Log the raw parameter value --- + // JToken rawIncludeFlag = @params["includeNonPublicSerialized"]; + // Debug.Log($"[HandleCommand Debug] Raw includeNonPublicSerialized parameter: Type={rawIncludeFlag?.Type.ToString() ?? "Null"}, Value={rawIncludeFlag?.ToString() ?? "N/A"}"); + // --- END DEBUG --- + string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) { @@ -37,6 +43,13 @@ public static object HandleCommand(JObject @params) string layer = @params["layer"]?.ToString(); JToken parentToken = @params["parent"]; + // --- Add parameter for controlling non-public field inclusion --- + // Reverting to original logic, assuming external system will be fixed to send the parameter correctly. + bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject() ?? true; // Default to true + // Revised: Explicitly check for null, default to false if null/missing. -- REMOVED + // bool includeNonPublicSerialized = @params["includeNonPublicSerialized"] != null && @params["includeNonPublicSerialized"].ToObject(); + // --- End add parameter --- + // --- Prefab Redirection Check --- string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; @@ -125,7 +138,8 @@ public static object HandleCommand(JObject @params) return Response.Error( "'target' parameter required for get_components." ); - return GetComponentsFromTarget(getCompTarget, searchMethod); + // Pass the includeNonPublicSerialized flag here + return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized); case "add_component": return AddComponentToTarget(@params, targetToken, searchMethod); case "remove_component": @@ -865,7 +879,7 @@ string searchMethod return Response.Success($"Found {results.Count} GameObject(s).", results); } - private static object GetComponentsFromTarget(string target, string searchMethod) + private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized) { GameObject targetGo = FindObjectInternal(target, searchMethod); if (targetGo == null) @@ -878,7 +892,8 @@ private static object GetComponentsFromTarget(string target, string searchMethod try { Component[] components = targetGo.GetComponents(); - var componentData = components.Select(c => GetComponentData(c)).ToList(); + // Pass the flag to GetComponentData + var componentData = components.Select(c => GetComponentData(c, includeNonPublicSerialized)).ToList(); return Response.Success( $"Retrieved {componentData.Count} components from '{targetGo.name}'.", componentData @@ -1815,6 +1830,7 @@ private static object ConvertJTokenToType(JToken token, Type targetType) string materialPath = token["path"]?.ToString(); if (!string.IsNullOrEmpty(materialPath)) { +#if UNITY_EDITOR // AssetDatabase is editor-only // Load the material by path Material material = AssetDatabase.LoadAssetAtPath(materialPath); if (material != null) @@ -1836,9 +1852,14 @@ private static object ConvertJTokenToType(JToken token, Type targetType) ); return null; } +#else + Debug.LogWarning("[ConvertJTokenToType] Material loading by path is only supported in the Unity Editor."); + return null; +#endif } // If no path is specified, could be a dynamic material or instance set by reference + // In a build, we can't load by path, so we rely on direct reference or null. return null; } @@ -1970,6 +1991,7 @@ private static object ConvertJTokenToType(JToken token, Type targetType) string assetPath = token.ToString(); if (!string.IsNullOrEmpty(assetPath)) { +#if UNITY_EDITOR // AssetDatabase is editor-only // Attempt to load the asset from the provided path using the target type UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath( assetPath, @@ -1983,10 +2005,13 @@ private static object ConvertJTokenToType(JToken token, Type targetType) { // Log a warning if the asset could not be found at the path Debug.LogWarning( - $"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists." - ); + $"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists."); return null; } +#else + Debug.LogWarning($"[ConvertJTokenToType] Asset loading by path ('{assetPath}') is only supported in the Unity Editor."); + return null; +#endif } else { @@ -2181,77 +2206,174 @@ private static object GetGameObjectData(GameObject go) }; } + // --- Metadata Caching for Reflection --- + private class CachedMetadata + { + public readonly List SerializableProperties; + public readonly List SerializableFields; + + public CachedMetadata(List properties, List fields) + { + SerializableProperties = properties; + SerializableFields = fields; + } + } + // Key becomes Tuple + private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>(); + // --- End Metadata Caching --- + /// /// Creates a serializable representation of a Component, attempting to serialize - /// public properties and fields using reflection. + /// public properties and fields using reflection, with caching and control over non-public fields. /// - private static object GetComponentData(Component c) + // Add the flag parameter here + private static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) { if (c == null) return null; + Type componentType = c.GetType(); + + // TEMP: Clear cache for testing again -- REMOVING + // _metadataCache.Clear(); var data = new Dictionary { - { "typeName", c.GetType().FullName }, + { "typeName", componentType.FullName }, { "instanceID", c.GetInstanceID() } }; - var serializableProperties = new Dictionary(); - Type componentType = c.GetType(); - // Include NonPublic flags for fields, keep Public for properties initially - BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; + // --- Get Cached or Generate Metadata (using new cache key) --- + // _metadataCache.Clear(); // TEMP: Clear cache for testing - REMOVED + Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields); + if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) + { + // ---- ADD THIS ---- + // UnityEngine.Debug.Log($"[MCP Cache Test] Metadata MISS for Type: {componentType.FullName}, IncludeNonPublic: {includeNonPublicSerializedFields}. Generating..."); + // ----------------- + var propertiesToCache = new List(); + var fieldsToCache = new List(); +//test + // Traverse the hierarchy from the component type up to MonoBehaviour + Type currentType = componentType; + while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) + { + // Get properties declared only at the current type level + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + foreach (var propInfo in currentType.GetProperties(propFlags)) + { + // Basic filtering (readable, not indexer, not transform which is handled elsewhere) + if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + // Add if not already added (handles overrides - keep the most derived version) + if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) { + propertiesToCache.Add(propInfo); + } + } - // Process Properties (Still only public for properties) - // Using propFlags here - foreach (var propInfo in componentType.GetProperties(propFlags)) - { - // Skip indexers and write-only properties, and skip the transform property as it's handled by GetGameObjectData - if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + // Get fields declared only at the current type level (both public and non-public) + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var declaredFields = currentType.GetFields(fieldFlags); + + // Process the declared Fields for caching + foreach (var fieldInfo in declaredFields) + { + if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields + + // Add if not already added (handles hiding - keep the most derived version) + if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; + + bool shouldInclude = false; + if (includeNonPublicSerializedFields) + { + // If TRUE, include Public OR NonPublic with [SerializeField] + shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false)); + } + else // includeNonPublicSerializedFields is FALSE + { + // If FALSE, include ONLY if it is explicitly Public. + shouldInclude = fieldInfo.IsPublic; + } + + if (shouldInclude) + { + fieldsToCache.Add(fieldInfo); + } + } + + // Move to the base type + currentType = currentType.BaseType; + } + // --- End Hierarchy Traversal --- + + // REMOVED Original non-hierarchical property/field gathering logic + /* + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + foreach (var propInfo in componentType.GetProperties(propFlags)) { ... } + var allQueriedFields = componentType.GetFields(fieldFlags); + foreach (var fieldInfo in allQueriedFields) { ... } + */ + + cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); + _metadataCache[cacheKey] = cachedData; // Add to cache with combined key + } + // ---- ADD THIS ---- + // UnityEngine.Debug.Log($"[MCP Cache Test] Metadata HIT for Type: {componentType.FullName}, IncludeNonPublic: {includeNonPublicSerializedFields}. Using cache."); + // ----------------- + // --- End Get Cached or Generate Metadata --- + + // --- Use cached metadata (no changes needed here) --- + var serializablePropertiesOutput = new Dictionary(); + // Use cached properties + foreach (var propInfo in cachedData.SerializableProperties) + { + // --- Skip known obsolete/problematic Component shortcut properties --- + string propName = propInfo.Name; + if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || + propName == "light" || propName == "animation" || propName == "constantForce" || + propName == "renderer" || propName == "audio" || propName == "networkView" || + propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || + propName == "particleSystem" || + // Also skip potentially problematic Matrix properties prone to cycles/errors + propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") + { + continue; // Skip these properties + } + // --- End Skip --- try { object value = propInfo.GetValue(c); - string propName = propInfo.Name; + // string propName = propInfo.Name; // Moved up Type propType = propInfo.PropertyType; - - AddSerializableValue(serializableProperties, propName, propType, value); + AddSerializableValue(serializablePropertiesOutput, propName, propType, value); } catch (Exception ex) { - Debug.LogWarning($"Could not read property {propInfo.Name} on {componentType.Name}: {ex.Message}"); + Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); } } - // Process Fields (Include NonPublic) - // Using fieldFlags here - foreach (var fieldInfo in componentType.GetFields(fieldFlags)) + // Use cached fields + foreach (var fieldInfo in cachedData.SerializableFields) { - // Skip backing fields for properties (common pattern) - if (fieldInfo.Name.EndsWith("k__BackingField")) continue; - - // Only include public fields or non-public fields with [SerializeField] - // Check if the field is explicitly marked with SerializeField or if it's public - bool isSerializable = fieldInfo.IsPublic || fieldInfo.IsDefined(typeof(SerializeField), inherit: false); // inherit: false is typical for SerializeField - - if (!isSerializable) continue; // Skip if not public and not explicitly serialized - try { object value = fieldInfo.GetValue(c); string fieldName = fieldInfo.Name; Type fieldType = fieldInfo.FieldType; - - AddSerializableValue(serializableProperties, fieldName, fieldType, value); + AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); } catch (Exception ex) { + // Corrected: Use fieldInfo.Name here as fieldName is out of scope Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); } } + // --- End Use cached metadata --- - if (serializableProperties.Count > 0) + if (serializablePropertiesOutput.Count > 0) { - data["properties"] = serializableProperties; // Add the collected properties + data["properties"] = serializablePropertiesOutput; } return data; @@ -2282,16 +2404,23 @@ private static void AddSerializableValue(Dictionary dict, string { var obj = value as UnityEngine.Object; if (obj != null) { - // Use dynamic or a helper class for flexible properties if adding assetPath var refData = new Dictionary { { "name", obj.name }, { "instanceID", obj.GetInstanceID() }, { "typeName", obj.GetType().FullName } }; + // Attempt to get asset path and GUID +#if UNITY_EDITOR // AssetDatabase is editor-only string assetPath = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(assetPath)) { refData["assetPath"] = assetPath; + // Add GUID if asset path exists + string guid = AssetDatabase.AssetPathToGUID(assetPath); + if (!string.IsNullOrEmpty(guid)) { + refData["guid"] = guid; + } } +#endif dict[name] = refData; } else { @@ -2302,16 +2431,46 @@ private static void AddSerializableValue(Dictionary dict, string else if (type == typeof(List)) { dict[name] = value as List; // Directly serializable } - else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { - // Could attempt to serialize lists of primitives/structs/references here if needed - dict[name] = $"[Skipped List<{type.GetGenericArguments()[0].Name}>]"; + // Explicit handling for List + else if (type == typeof(List)) { + var vectorList = value as List; + if (vectorList != null) { + // Serialize each Vector3 into a list of dictionaries + var serializableList = vectorList.Select(v => new Dictionary { + { "x", v.x }, + { "y", v.y }, + { "z", v.z } + }).ToList(); + dict[name] = serializableList; + } else { + dict[name] = null; // Or an empty list, or an error message + } } - else if (type.IsArray) { - dict[name] = $"[Skipped Array<{type.GetElementType().Name}>]"; - } - // Skip other complex types for now + // Attempt to serialize other complex types using JToken else { - dict[name] = $"[Skipped complex type: {type.FullName}]"; + // UnityEngine.Debug.Log($"[MCP Debug] Attempting JToken serialization for field: {name} (Type: {type.FullName})"); // Removed this debug log + try + { + // Let Newtonsoft.Json attempt to serialize the value into a JToken + JToken jValue = JToken.FromObject(value); + // We store the JToken itself; the final JSON serialization will handle it. + // Important: Avoid potential cycles by not serializing excessively deep objects here. + // JToken.FromObject handles basic cycle detection, but complex scenarios might still occur. + // Consider adding depth limits if necessary. + dict[name] = jValue; + } + catch (JsonSerializationException jsonEx) + { + // Handle potential serialization issues (e.g., cycles, unsupported types) + Debug.LogWarning($"[AddSerializableValue] Could not serialize complex type '{type.FullName}' for property '{name}' using JToken: {jsonEx.Message}. Storing skip message."); + dict[name] = $"[Serialization Error: {type.FullName} - {jsonEx.Message}]"; + } + catch (Exception ex) + { + // Catch other unexpected errors during serialization + Debug.LogWarning($"[AddSerializableValue] Unexpected error serializing complex type '{type.FullName}' for property '{name}' using JToken: {ex.Message}"); + dict[name] = $"[Serialization Error: {type.FullName} - Unexpected]"; + } } } } diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 9276c05b..a0b112b2 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -267,6 +267,7 @@ private static void ProcessCommands() // Normal JSON command processing Command command = JsonConvert.DeserializeObject(commandText); + if (command == null) { var nullCommandResponse = new diff --git a/UnityMcpServer/src/tools/manage_gameobject.py b/UnityMcpServer/src/tools/manage_gameobject.py index a65331fa..0f4c9bff 100644 --- a/UnityMcpServer/src/tools/manage_gameobject.py +++ b/UnityMcpServer/src/tools/manage_gameobject.py @@ -35,6 +35,7 @@ def manage_gameobject( search_inactive: bool = False, # -- Component Management Arguments -- component_name: str = None, + includeNonPublicSerialized: bool = None, # Controls serialization of private [SerializeField] fields ) -> Dict[str, Any]: """Manages GameObjects: create, modify, delete, find, and component operations. @@ -59,6 +60,7 @@ def manage_gameobject( Action-specific arguments (e.g., position, rotation, scale for create/modify; component_name for component actions; search_term, find_all for 'find'). + includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data. Returns: Dictionary with operation results ('success', 'message', 'data'). @@ -91,7 +93,8 @@ def manage_gameobject( "findAll": find_all, "searchInChildren": search_in_children, "searchInactive": search_inactive, - "componentName": component_name + "componentName": component_name, + "includeNonPublicSerialized": includeNonPublicSerialized } params = {k: v for k, v in params.items() if v is not None} diff --git a/UnityMcpServer/src/uv.lock b/UnityMcpServer/src/uv.lock index 2f8a4d59..bc3e54ca 100644 --- a/UnityMcpServer/src/uv.lock +++ b/UnityMcpServer/src/uv.lock @@ -321,8 +321,8 @@ wheels = [ ] [[package]] -name = "unity-mcp" -version = "1.0.1" +name = "unitymcpserver" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From dd0113d258e9f69a3823aa4a62ea9e8d59f66fcf Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 09:42:42 -0700 Subject: [PATCH 3/7] Refactor: Extract GameObject/Component serialization to GameObjectSerializer helper Moved serialization logic (GetGameObjectData, GetComponentData, metadata caching, JSON conversion helpers) from ManageGameObject tool to a dedicated GameObjectSerializer class in the Helpers namespace. This improves separation of concerns and reduces the size/complexity of ManageGameObject.cs. Updated ManageGameObject to use the new helper class. --- .../Editor/Helpers/GameObjectSerializer.cs | 378 ++++++ .../Editor/Tools/ManageGameObject.cs | 1158 ++++++----------- 2 files changed, 746 insertions(+), 790 deletions(-) create mode 100644 UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs new file mode 100644 index 00000000..8a65ada3 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using UnityMcpBridge.Runtime.Serialization; // For Converters + +namespace UnityMcpBridge.Editor.Helpers +{ + /// + /// Handles serialization of GameObjects and Components for MCP responses. + /// Includes reflection helpers and caching for performance. + /// + public static class GameObjectSerializer + { + // --- Data Serialization --- + + /// + /// Creates a serializable representation of a GameObject. + /// + public static object GetGameObjectData(GameObject go) + { + if (go == null) + return null; + return new + { + name = go.name, + instanceID = go.GetInstanceID(), + tag = go.tag, + layer = go.layer, + activeSelf = go.activeSelf, + activeInHierarchy = go.activeInHierarchy, + isStatic = go.isStatic, + scenePath = go.scene.path, // Identify which scene it belongs to + transform = new // Serialize transform components carefully to avoid JSON issues + { + // Serialize Vector3 components individually to prevent self-referencing loops. + // The default serializer can struggle with properties like Vector3.normalized. + position = new + { + x = go.transform.position.x, + y = go.transform.position.y, + z = go.transform.position.z, + }, + localPosition = new + { + x = go.transform.localPosition.x, + y = go.transform.localPosition.y, + z = go.transform.localPosition.z, + }, + rotation = new + { + x = go.transform.rotation.eulerAngles.x, + y = go.transform.rotation.eulerAngles.y, + z = go.transform.rotation.eulerAngles.z, + }, + localRotation = new + { + x = go.transform.localRotation.eulerAngles.x, + y = go.transform.localRotation.eulerAngles.y, + z = go.transform.localRotation.eulerAngles.z, + }, + scale = new + { + x = go.transform.localScale.x, + y = go.transform.localScale.y, + z = go.transform.localScale.z, + }, + forward = new + { + x = go.transform.forward.x, + y = go.transform.forward.y, + z = go.transform.forward.z, + }, + up = new + { + x = go.transform.up.x, + y = go.transform.up.y, + z = go.transform.up.z, + }, + right = new + { + x = go.transform.right.x, + y = go.transform.right.y, + z = go.transform.right.z, + }, + }, + parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent + // Optionally include components, but can be large + // components = go.GetComponents().Select(c => GetComponentData(c)).ToList() + // Or just component names: + componentNames = go.GetComponents() + .Select(c => c.GetType().FullName) + .ToList(), + }; + } + + // --- Metadata Caching for Reflection --- + private class CachedMetadata + { + public readonly List SerializableProperties; + public readonly List SerializableFields; + + public CachedMetadata(List properties, List fields) + { + SerializableProperties = properties; + SerializableFields = fields; + } + } + // Key becomes Tuple + private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>(); + // --- End Metadata Caching --- + + /// + /// Creates a serializable representation of a Component, attempting to serialize + /// public properties and fields using reflection, with caching and control over non-public fields. + /// + // Add the flag parameter here + public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) + { + if (c == null) return null; + Type componentType = c.GetType(); + + var data = new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", c.GetInstanceID() } + }; + + // --- Get Cached or Generate Metadata (using new cache key) --- + Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields); + if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) + { + var propertiesToCache = new List(); + var fieldsToCache = new List(); + + // Traverse the hierarchy from the component type up to MonoBehaviour + Type currentType = componentType; + while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) + { + // Get properties declared only at the current type level + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + foreach (var propInfo in currentType.GetProperties(propFlags)) + { + // Basic filtering (readable, not indexer, not transform which is handled elsewhere) + if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + // Add if not already added (handles overrides - keep the most derived version) + if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) { + propertiesToCache.Add(propInfo); + } + } + + // Get fields declared only at the current type level (both public and non-public) + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var declaredFields = currentType.GetFields(fieldFlags); + + // Process the declared Fields for caching + foreach (var fieldInfo in declaredFields) + { + if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields + + // Add if not already added (handles hiding - keep the most derived version) + if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; + + bool shouldInclude = false; + if (includeNonPublicSerializedFields) + { + // If TRUE, include Public OR NonPublic with [SerializeField] + shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false)); + } + else // includeNonPublicSerializedFields is FALSE + { + // If FALSE, include ONLY if it is explicitly Public. + shouldInclude = fieldInfo.IsPublic; + } + + if (shouldInclude) + { + fieldsToCache.Add(fieldInfo); + } + } + + // Move to the base type + currentType = currentType.BaseType; + } + // --- End Hierarchy Traversal --- + + cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); + _metadataCache[cacheKey] = cachedData; // Add to cache with combined key + } + // --- End Get Cached or Generate Metadata --- + + // --- Use cached metadata --- + var serializablePropertiesOutput = new Dictionary(); + // Use cached properties + foreach (var propInfo in cachedData.SerializableProperties) + { + // --- Skip known obsolete/problematic Component shortcut properties --- + string propName = propInfo.Name; + if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || + propName == "light" || propName == "animation" || propName == "constantForce" || + propName == "renderer" || propName == "audio" || propName == "networkView" || + propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || + propName == "particleSystem" || + // Also skip potentially problematic Matrix properties prone to cycles/errors + propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") + { + continue; // Skip these properties + } + // --- End Skip --- + + try + { + object value = propInfo.GetValue(c); + Type propType = propInfo.PropertyType; + AddSerializableValue(serializablePropertiesOutput, propName, propType, value); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); + } + } + + // Use cached fields + foreach (var fieldInfo in cachedData.SerializableFields) + { + try + { + object value = fieldInfo.GetValue(c); + string fieldName = fieldInfo.Name; + Type fieldType = fieldInfo.FieldType; + AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); + } + } + // --- End Use cached metadata --- + + if (serializablePropertiesOutput.Count > 0) + { + data["properties"] = serializablePropertiesOutput; + } + + return data; + } + + // Helper function to decide how to serialize different types + private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) + { + // Simplified: Directly use CreateTokenFromValue which uses the serializer + if (value == null) + { + dict[name] = null; + return; + } + + try + { + // Use the helper that employs our custom serializer settings + JToken token = CreateTokenFromValue(value, type); + if (token != null) // Check if serialization succeeded in the helper + { + // Convert JToken back to a basic object structure for the dictionary + dict[name] = ConvertJTokenToPlainObject(token); + } + // If token is null, it means serialization failed and a warning was logged. + } + catch (Exception e) + { + // Catch potential errors during JToken conversion or addition to dictionary + Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); + } + } + + // Helper to convert JToken back to basic object structure + private static object ConvertJTokenToPlainObject(JToken token) + { + if (token == null) return null; + + switch (token.Type) + { + case JTokenType.Object: + var objDict = new Dictionary(); + foreach (var prop in ((JObject)token).Properties()) + { + objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value); + } + return objDict; + + case JTokenType.Array: + var list = new List(); + foreach (var item in (JArray)token) + { + list.Add(ConvertJTokenToPlainObject(item)); + } + return list; + + case JTokenType.Integer: + return token.ToObject(); // Use long for safety + case JTokenType.Float: + return token.ToObject(); // Use double for safety + case JTokenType.String: + return token.ToObject(); + case JTokenType.Boolean: + return token.ToObject(); + case JTokenType.Date: + return token.ToObject(); + case JTokenType.Guid: + return token.ToObject(); + case JTokenType.Uri: + return token.ToObject(); + case JTokenType.TimeSpan: + return token.ToObject(); + case JTokenType.Bytes: + return token.ToObject(); + case JTokenType.Null: + return null; + case JTokenType.Undefined: + return null; // Treat undefined as null + + default: + // Fallback for simple value types not explicitly listed + if (token is JValue jValue && jValue.Value != null) + { + return jValue.Value; + } + Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); + return null; + } + } + + // --- Define custom JsonSerializerSettings for OUTPUT --- + private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings + { + Converters = new List + { + new Vector3Converter(), + new Vector2Converter(), + new QuaternionConverter(), + new ColorConverter(), + new RectConverter(), + new BoundsConverter(), + new UnityEngineObjectConverter() // Handles serialization of references + }, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed + }; + private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings); + // --- End Define custom JsonSerializerSettings --- + + // Helper to create JToken using the output serializer + private static JToken CreateTokenFromValue(object value, Type type) + { + if (value == null) return JValue.CreateNull(); + + try + { + // Use the pre-configured OUTPUT serializer instance + return JToken.FromObject(value, _outputSerializer); + } + catch (JsonSerializationException e) + { + Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); + return null; // Indicate serialization failure + } + catch (Exception e) // Catch other unexpected errors + { + Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); + return null; // Indicate serialization failure + } + } + } +} \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 75c4a62b..1e22ebd9 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -9,7 +9,8 @@ using UnityEditorInternal; using UnityEngine; using UnityEngine.SceneManagement; -using UnityMcpBridge.Editor.Helpers; // For Response class +using UnityMcpBridge.Editor.Helpers; // For Response class AND GameObjectSerializer +using UnityMcpBridge.Runtime.Serialization; // <<< Keep for Converters access? Might not be needed here directly namespace UnityMcpBridge.Editor.Tools { @@ -44,10 +45,7 @@ public static object HandleCommand(JObject @params) JToken parentToken = @params["parent"]; // --- Add parameter for controlling non-public field inclusion --- - // Reverting to original logic, assuming external system will be fixed to send the parameter correctly. bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject() ?? true; // Default to true - // Revised: Explicitly check for null, default to false if null/missing. -- REMOVED - // bool includeNonPublicSerialized = @params["includeNonPublicSerialized"] != null && @params["includeNonPublicSerialized"].ToObject(); // --- End add parameter --- // --- Prefab Redirection Check --- @@ -217,29 +215,21 @@ private static object CreateGameObject(JObject @params) else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { // If it looks like a path but doesn't end with .prefab, assume user forgot it and append it. - // We could also error here, but appending might be more user-friendly. Debug.LogWarning( $"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending." ); prefabPath += ".prefab"; - // Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that. } - // Removed the early return error for missing .prefab ending. - // The logic above now handles finding or assuming the .prefab extension. - GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); if (prefabAsset != null) { try { - // Instantiate the prefab, initially place it at the root - // Parent will be set later if specified newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; if (newGo == null) { - // This might happen if the asset exists but isn't a valid GameObject prefab somehow Debug.LogError( $"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject." ); @@ -248,13 +238,11 @@ private static object CreateGameObject(JObject @params) ); } - // Name the instance based on the 'name' parameter, not the prefab's default name if (!string.IsNullOrEmpty(name)) { newGo.name = name; } - // Register Undo for prefab instantiation Undo.RegisterCreatedObjectUndo( newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'" @@ -272,12 +260,9 @@ private static object CreateGameObject(JObject @params) } else { - // Only return error if prefabPath was specified but not found. - // If prefabPath was empty/null, we proceed to create primitive/empty. Debug.LogWarning( $"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified." ); - // Do not return error here, allow fallback to primitive/empty creation } } @@ -292,7 +277,6 @@ private static object CreateGameObject(JObject @params) PrimitiveType type = (PrimitiveType) Enum.Parse(typeof(PrimitiveType), primitiveType, true); newGo = GameObject.CreatePrimitive(type); - // Set name *after* creation for primitives if (!string.IsNullOrEmpty(name)) newGo.name = name; else @@ -326,22 +310,17 @@ private static object CreateGameObject(JObject @params) createdNewObject = true; } - // Record creation for Undo *only* if we created a new object if (createdNewObject) { Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); } } - // --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists --- if (newGo == null) { - // Should theoretically not happen if logic above is correct, but safety check. return Response.Error("Failed to create or instantiate the GameObject."); } - // Record potential changes to the existing prefab instance or the new GO - // Record transform separately in case parent changes affect it Undo.RecordObject(newGo.transform, "Set GameObject Transform"); Undo.RecordObject(newGo, "Set GameObject Properties"); @@ -373,7 +352,6 @@ private static object CreateGameObject(JObject @params) // Set Tag (added for create action) if (!string.IsNullOrEmpty(tag)) { - // Similar logic as in ModifyGameObject for setting/creating tags string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; try { @@ -470,16 +448,13 @@ private static object CreateGameObject(JObject @params) if (createdNewObject && saveAsPrefab) { string finalPrefabPath = prefabPath; // Use a separate variable for saving path - // This check should now happen *before* attempting to save if (string.IsNullOrEmpty(finalPrefabPath)) { - // Clean up the created object before returning error UnityEngine.Object.DestroyImmediate(newGo); return Response.Error( "'prefabPath' is required when 'saveAsPrefab' is true and creating a new object." ); } - // Ensure the *saving* path ends with .prefab if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { Debug.Log( @@ -488,16 +463,8 @@ private static object CreateGameObject(JObject @params) finalPrefabPath += ".prefab"; } - // Removed the error check here as we now ensure the extension exists - // if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) - // { - // UnityEngine.Object.DestroyImmediate(newGo); - // return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'"); - // } - try { - // Ensure directory exists using the final saving path string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); if ( !string.IsNullOrEmpty(directoryPath) @@ -511,7 +478,6 @@ private static object CreateGameObject(JObject @params) ); } - // Use SaveAsPrefabAssetAndConnect with the final saving path finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( newGo, finalPrefabPath, @@ -520,7 +486,6 @@ private static object CreateGameObject(JObject @params) if (finalInstance == null) { - // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) UnityEngine.Object.DestroyImmediate(newGo); return Response.Error( $"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions." @@ -529,21 +494,16 @@ private static object CreateGameObject(JObject @params) Debug.Log( $"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected." ); - // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. - // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect } catch (Exception e) { - // Clean up the instance if prefab saving fails UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); } } - // Select the instance in the scene (either prefab instance or newly created/saved one) Selection.activeGameObject = finalInstance; - // Determine appropriate success message using the potentially updated or original path string messagePrefabPath = finalInstance == null ? originalPrefabPath @@ -568,8 +528,8 @@ private static object CreateGameObject(JObject @params) $"GameObject '{finalInstance.name}' created successfully in scene."; } - // Return data for the instance in the scene - return Response.Success(successMessage, GetGameObjectData(finalInstance)); + // Use the new serializer helper + return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); } private static object ModifyGameObject( @@ -586,7 +546,6 @@ string searchMethod ); } - // Record state for Undo *before* modifications Undo.RecordObject(targetGo.transform, "Modify GameObject Transform"); Undo.RecordObject(targetGo, "Modify GameObject Properties"); @@ -618,7 +577,6 @@ string searchMethod { return Response.Error($"New parent ('{parentToken}') not found."); } - // Check for hierarchy loops if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) { return Response.Error( @@ -642,22 +600,16 @@ string searchMethod // Change Tag (using consolidated 'tag' parameter) string tag = @params["tag"]?.ToString(); - // Only attempt to change tag if a non-null tag is provided and it's different from the current one. - // Allow setting an empty string to remove the tag (Unity uses "Untagged"). if (tag != null && targetGo.tag != tag) { - // Ensure the tag is not empty, if empty, it means "Untagged" implicitly string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; - try { - // First attempt to set the tag targetGo.tag = tagToSet; modified = true; } catch (UnityException ex) { - // Check if the error is specifically because the tag doesn't exist if (ex.Message.Contains("is not defined")) { Debug.LogWarning( @@ -665,21 +617,15 @@ string searchMethod ); try { - // Attempt to create the tag using internal utility InternalEditorUtility.AddTag(tagToSet); - // Wait a frame maybe? Not strictly necessary but sometimes helps editor updates. - // yield return null; // Cannot yield here, editor script limitation - - // Retry setting the tag immediately after creation targetGo.tag = tagToSet; - modified = true; // Mark as modified on successful retry + modified = true; Debug.Log( $"[ManageGameObject] Tag '{tagToSet}' created and assigned successfully." ); } catch (Exception innerEx) { - // Handle failure during tag creation or the second assignment attempt Debug.LogError( $"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}" ); @@ -690,7 +636,6 @@ string searchMethod } else { - // If the exception was for a different reason, return the original error return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}."); } } @@ -736,7 +681,6 @@ string searchMethod } // --- Component Modifications --- - // Note: These might need more specific Undo recording per component // Remove Components if (@params["componentsToRemove"] is JArray componentsToRemoveArray) @@ -759,7 +703,6 @@ string searchMethod { foreach (var compToken in componentsToAddArrayModify) { - // ... (parsing logic as in CreateGameObject) ... string typeName = null; JObject properties = null; if (compToken.Type == JTokenType.String) @@ -803,16 +746,18 @@ string searchMethod if (!modified) { + // Use the new serializer helper return Response.Success( $"No modifications applied to GameObject '{targetGo.name}'.", - GetGameObjectData(targetGo) + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); } EditorUtility.SetDirty(targetGo); // Mark scene as dirty + // Use the new serializer helper return Response.Success( $"GameObject '{targetGo.name}' modified successfully.", - GetGameObjectData(targetGo) + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); } @@ -875,7 +820,8 @@ string searchMethod return Response.Success("No matching GameObjects found.", new List()); } - var results = foundObjects.Select(go => GetGameObjectData(go)).ToList(); + // Use the new serializer helper + var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList(); return Response.Success($"Found {results.Count} GameObject(s).", results); } @@ -892,8 +838,8 @@ private static object GetComponentsFromTarget(string target, string searchMethod try { Component[] components = targetGo.GetComponents(); - // Pass the flag to GetComponentData - var componentData = components.Select(c => GetComponentData(c, includeNonPublicSerialized)).ToList(); + // Use the new serializer helper and pass the flag + var componentData = components.Select(c => Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized)).ToList(); return Response.Success( $"Retrieved {componentData.Count} components from '{targetGo.name}'.", componentData @@ -957,9 +903,10 @@ string searchMethod return addResult; // Return error EditorUtility.SetDirty(targetGo); + // Use the new serializer helper return Response.Success( $"Component '{typeName}' added to '{targetGo.name}'.", - GetGameObjectData(targetGo) + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); // Return updated GO data } @@ -1003,9 +950,10 @@ string searchMethod return removeResult; // Return error EditorUtility.SetDirty(targetGo); + // Use the new serializer helper return Response.Success( $"Component '{typeName}' removed from '{targetGo.name}'.", - GetGameObjectData(targetGo) + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); } @@ -1051,9 +999,10 @@ string searchMethod return setResult; // Return error EditorUtility.SetDirty(targetGo); + // Use the new serializer helper return Response.Success( $"Properties set for component '{compName}' on '{targetGo.name}'.", - GetGameObjectData(targetGo) + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); } @@ -1330,11 +1279,6 @@ JObject properties } } - // Check if component already exists (optional, depending on desired behavior) - // if (targetGo.GetComponent(componentType) != null) { - // return Response.Error($"Component '{typeName}' already exists on '{targetGo.name}'."); - // } - try { // Use Undo.AddComponent for undo support @@ -1454,8 +1398,6 @@ private static object SetComponentPropertiesInternal( Debug.LogWarning( $"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch." ); - // Optionally return an error here instead of just logging - // return Response.Error($"Could not set property '{propName}' on component '{compName}'."); } } catch (Exception e) @@ -1463,8 +1405,6 @@ private static object SetComponentPropertiesInternal( Debug.LogError( $"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}" ); - // Optionally return an error here - // return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}"); } } EditorUtility.SetDirty(targetComponent); @@ -1480,43 +1420,71 @@ private static bool SetProperty(object target, string memberName, JToken value) BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + // --- Use a dedicated serializer for input conversion --- + // Define this somewhere accessible, maybe static readonly field + JsonSerializerSettings inputSerializerSettings = new JsonSerializerSettings + { + Converters = new List + { + // Add specific converters needed for INPUT deserialization if different from output + new Vector3Converter(), + new Vector2Converter(), + new QuaternionConverter(), + new ColorConverter(), + new RectConverter(), + new BoundsConverter(), + new UnityEngineObjectConverter() // Crucial for finding references from instructions + } + // No ReferenceLoopHandling needed typically for input + }; + JsonSerializer inputSerializer = JsonSerializer.Create(inputSerializerSettings); + // --- End Serializer Setup --- + try { // Handle special case for materials with dot notation (material.property) - // Examples: material.color, sharedMaterial.color, materials[0].color if (memberName.Contains('.') || memberName.Contains('[')) { - return SetNestedProperty(target, memberName, value); + // Pass the inputSerializer down for nested conversions + return SetNestedProperty(target, memberName, value, inputSerializer); } PropertyInfo propInfo = type.GetProperty(memberName, flags); if (propInfo != null && propInfo.CanWrite) { - object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); - if (convertedValue != null) + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null { propInfo.SetValue(target, convertedValue); return true; } + else { + Debug.LogWarning($"[SetProperty] Conversion failed for property '{memberName}' (Type: {propInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); + } } else { FieldInfo fieldInfo = type.GetField(memberName, flags); - if (fieldInfo != null) + if (fieldInfo != null) // Check if !IsLiteral? { - object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); - if (convertedValue != null) + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null { fieldInfo.SetValue(target, convertedValue); return true; } + else { + Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } } } } catch (Exception ex) { Debug.LogError( - $"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}" + $"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}" ); } return false; @@ -1525,7 +1493,8 @@ private static bool SetProperty(object target, string memberName, JToken value) /// /// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]") /// - private static bool SetNestedProperty(object target, string path, JToken value) + // Pass the input serializer for conversions + private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) { try { @@ -1546,7 +1515,6 @@ private static bool SetNestedProperty(object target, string path, JToken value) bool isArray = false; int arrayIndex = -1; - // Check if this part contains array indexing if (part.Contains("[")) { int startBracket = part.IndexOf('['); @@ -1565,7 +1533,6 @@ private static bool SetNestedProperty(object target, string path, JToken value) } } - // Get the property/field PropertyInfo propInfo = currentType.GetProperty(part, flags); FieldInfo fieldInfo = null; if (propInfo == null) @@ -1580,13 +1547,11 @@ private static bool SetNestedProperty(object target, string path, JToken value) } } - // Get the value currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject); - // If the current property is null, we need to stop if (currentObject == null) { Debug.LogWarning( @@ -1595,7 +1560,6 @@ private static bool SetNestedProperty(object target, string path, JToken value) return false; } - // If this is an array/list access, get the element at the index if (isArray) { if (currentObject is Material[]) @@ -1630,8 +1594,6 @@ private static bool SetNestedProperty(object target, string path, JToken value) return false; } } - - // Update type for next iteration currentType = currentObject.GetType(); } @@ -1641,94 +1603,41 @@ private static bool SetNestedProperty(object target, string path, JToken value) // Special handling for Material properties (shader properties) if (currentObject is Material material && finalPart.StartsWith("_")) { - // Handle various material property types + // Use the serializer to convert the JToken value first if (value is JArray jArray) { - if (jArray.Count == 4) // Color with alpha - { - Color color = new Color( - jArray[0].ToObject(), - jArray[1].ToObject(), - jArray[2].ToObject(), - jArray[3].ToObject() - ); - material.SetColor(finalPart, color); - return true; - } - else if (jArray.Count == 3) // Color without alpha - { - Color color = new Color( - jArray[0].ToObject(), - jArray[1].ToObject(), - jArray[2].ToObject(), - 1.0f - ); - material.SetColor(finalPart, color); - return true; - } - else if (jArray.Count == 2) // Vector2 - { - Vector2 vec = new Vector2( - jArray[0].ToObject(), - jArray[1].ToObject() - ); - material.SetVector(finalPart, vec); - return true; - } - else if (jArray.Count == 4) // Vector4 - { - Vector4 vec = new Vector4( - jArray[0].ToObject(), - jArray[1].ToObject(), - jArray[2].ToObject(), - jArray[3].ToObject() - ); - material.SetVector(finalPart, vec); - return true; + // Try converting to known types that SetColor/SetVector accept + if (jArray.Count == 4) { + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch {} + try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {} + } else if (jArray.Count == 3) { + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch {} // ToObject handles conversion to Color + } else if (jArray.Count == 2) { + try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {} } } else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) { - material.SetFloat(finalPart, value.ToObject()); - return true; + try { material.SetFloat(finalPart, value.ToObject(inputSerializer)); return true; } catch {} } else if (value.Type == JTokenType.Boolean) { - material.SetFloat(finalPart, value.ToObject() ? 1f : 0f); - return true; + try { material.SetFloat(finalPart, value.ToObject(inputSerializer) ? 1f : 0f); return true; } catch {} } else if (value.Type == JTokenType.String) { - // Might be a texture path - string texturePath = value.ToString(); - if ( - texturePath.EndsWith(".png") - || texturePath.EndsWith(".jpg") - || texturePath.EndsWith(".tga") - ) - { - Texture2D texture = AssetDatabase.LoadAssetAtPath( - texturePath - ); - if (texture != null) - { - material.SetTexture(finalPart, texture); - return true; - } - } - else - { - // Materials don't have SetString, use SetTextureOffset as workaround or skip - // material.SetString(finalPart, texturePath); - Debug.LogWarning( - $"[SetNestedProperty] String values not directly supported for material property {finalPart}" - ); - return false; - } + // Try converting to Texture using the serializer/converter + try { + Texture texture = value.ToObject(inputSerializer); + if (texture != null) { + material.SetTexture(finalPart, texture); + return true; + } + } catch {} } Debug.LogWarning( - $"[SetNestedProperty] Unsupported material property value type: {value.Type} for {finalPart}" + $"[SetNestedProperty] Unsupported or failed conversion for material property '{finalPart}' from value: {value.ToString(Formatting.None)}" ); return false; } @@ -1737,32 +1646,37 @@ private static bool SetNestedProperty(object target, string path, JToken value) PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags); if (finalPropInfo != null && finalPropInfo.CanWrite) { - object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType); - if (convertedValue != null) + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) { finalPropInfo.SetValue(currentObject, convertedValue); return true; } + else { + Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); + } } else { FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); if (finalFieldInfo != null) { - object convertedValue = ConvertJTokenToType( - value, - finalFieldInfo.FieldType - ); - if (convertedValue != null) + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) { finalFieldInfo.SetValue(currentObject, convertedValue); return true; } + else { + Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } } else { Debug.LogWarning( - $"[SetNestedProperty] Could not find final property or field '{finalPart}' on type '{currentType.Name}'" + $"[SetNestedProperty] Could not find final writable property or field '{finalPart}' on type '{currentType.Name}'" ); } } @@ -1770,19 +1684,19 @@ private static bool SetNestedProperty(object target, string path, JToken value) catch (Exception ex) { Debug.LogError( - $"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}" + $"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}" ); } return false; } + /// /// Split a property path into parts, handling both dot notation and array indexers /// private static string[] SplitPropertyPath(string path) { - // Handle complex paths with both dots and array indexers List parts = new List(); int startIndex = 0; bool inBrackets = false; @@ -1801,264 +1715,260 @@ private static string[] SplitPropertyPath(string path) } else if (c == '.' && !inBrackets) { - // Found a dot separator outside of brackets parts.Add(path.Substring(startIndex, i - startIndex)); startIndex = i + 1; } } - - // Add the final part if (startIndex < path.Length) { parts.Add(path.Substring(startIndex)); } - return parts.ToArray(); } /// - /// Simple JToken to Type conversion for common Unity types. + /// Simple JToken to Type conversion for common Unity types, using JsonSerializer. /// - private static object ConvertJTokenToType(JToken token, Type targetType) + // Pass the input serializer + private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer) { - try + if (token == null || token.Type == JTokenType.Null) { - // Unwrap nested material properties if we're assigning to a Material - if (typeof(Material).IsAssignableFrom(targetType) && token is JObject materialProps) + if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) { - // Handle case where we're passing shader properties directly in a nested object - string materialPath = token["path"]?.ToString(); - if (!string.IsNullOrEmpty(materialPath)) - { -#if UNITY_EDITOR // AssetDatabase is editor-only - // Load the material by path - Material material = AssetDatabase.LoadAssetAtPath(materialPath); - if (material != null) - { - // If there are additional properties, set them - foreach (var prop in materialProps.Properties()) - { - if (prop.Name != "path") - { - SetProperty(material, prop.Name, prop.Value); - } - } - return material; - } - else - { - Debug.LogWarning( - $"[ConvertJTokenToType] Could not load material at path: '{materialPath}'" - ); - return null; - } -#else - Debug.LogWarning("[ConvertJTokenToType] Material loading by path is only supported in the Unity Editor."); - return null; -#endif - } - - // If no path is specified, could be a dynamic material or instance set by reference - // In a build, we can't load by path, so we rely on direct reference or null. - return null; + Debug.LogWarning($"Cannot assign null to non-nullable value type {targetType.Name}. Returning default value."); + return Activator.CreateInstance(targetType); } + return null; + } - // Basic types first - if (targetType == typeof(string)) - return token.ToObject(); - if (targetType == typeof(int)) - return token.ToObject(); - if (targetType == typeof(float)) - return token.ToObject(); - if (targetType == typeof(bool)) - return token.ToObject(); - - // Vector/Quaternion/Color types - if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) - return new Vector2(arrV2[0].ToObject(), arrV2[1].ToObject()); - if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) - return new Vector3( - arrV3[0].ToObject(), - arrV3[1].ToObject(), - arrV3[2].ToObject() - ); - if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) - return new Vector4( - arrV4[0].ToObject(), - arrV4[1].ToObject(), - arrV4[2].ToObject(), - arrV4[3].ToObject() - ); - if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) - return new Quaternion( - arrQ[0].ToObject(), - arrQ[1].ToObject(), - arrQ[2].ToObject(), - arrQ[3].ToObject() - ); - if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA - return new Color( - arrC[0].ToObject(), - arrC[1].ToObject(), - arrC[2].ToObject(), - arrC.Count > 3 ? arrC[3].ToObject() : 1.0f - ); - - // Enum types - if (targetType.IsEnum) - return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing - - // Handle assigning Unity Objects (Assets, Scene Objects, Components) - if (typeof(UnityEngine.Object).IsAssignableFrom(targetType)) - { - // CASE 1: Reference is a JSON Object specifying a scene object/component find criteria - if (token is JObject refObject) - { - JToken findToken = refObject["find"]; - string findMethod = - refObject["method"]?.ToString() ?? "by_id_or_name_or_path"; // Default search - string componentTypeName = refObject["component"]?.ToString(); - - if (findToken == null) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Reference object missing 'find' property: {token}" - ); - return null; - } - - // Find the target GameObject - // Pass 'searchInactive: true' for internal lookups to be more robust - JObject findParams = new JObject(); - findParams["searchInactive"] = true; - GameObject foundGo = FindObjectInternal(findToken, findMethod, findParams); - - if (foundGo == null) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Could not find GameObject specified by reference object: {token}" - ); - return null; - } - - // If a component type is specified, try to get it - if (!string.IsNullOrEmpty(componentTypeName)) - { - Type compType = FindType(componentTypeName); - if (compType == null) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Could not find component type '{componentTypeName}' specified in reference object: {token}" - ); - return null; - } + try + { + // Use the provided serializer instance which includes our custom converters + return token.ToObject(targetType, inputSerializer); + } + catch (JsonSerializationException jsonEx) + { + Debug.LogError($"JSON Deserialization Error converting token to {targetType.FullName}: {jsonEx.Message}\nToken: {token.ToString(Formatting.None)}"); + // Optionally re-throw or return null/default + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + throw; // Re-throw to indicate failure higher up + } + catch (ArgumentException argEx) + { + Debug.LogError($"Argument Error converting token to {targetType.FullName}: {argEx.Message}\nToken: {token.ToString(Formatting.None)}"); + throw; + } + catch (Exception ex) + { + Debug.LogError($"Unexpected error converting token to {targetType.FullName}: {ex}\nToken: {token.ToString(Formatting.None)}"); + throw; + } + // If ToObject succeeded, it would have returned. If it threw, we wouldn't reach here. + // This fallback logic is likely unreachable if ToObject covers all cases or throws on failure. + // Debug.LogWarning($"Conversion failed for token to {targetType.FullName}. Token: {token.ToString(Formatting.None)}"); + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } - // Ensure the targetType is assignable from the found component type - if (!targetType.IsAssignableFrom(compType)) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Found component '{componentTypeName}' but it is not assignable to the target property type '{targetType.Name}'. Reference: {token}" - ); - return null; - } + // --- ParseJTokenTo... helpers are likely redundant now with the serializer approach --- + // Keep them temporarily for reference or if specific fallback logic is ever needed. - Component foundComp = foundGo.GetComponent(compType); - if (foundComp == null) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but could not find component '{componentTypeName}' on it. Reference: {token}" - ); - return null; - } - return foundComp; // Return the found component - } - else - { - // Otherwise, return the GameObject itself, ensuring it's assignable - if (!targetType.IsAssignableFrom(typeof(GameObject))) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but it is not assignable to the target property type '{targetType.Name}' (component name was not specified). Reference: {token}" - ); - return null; - } - return foundGo; // Return the found GameObject - } - } - // CASE 2: Reference is a string, assume it's an asset path - else if (token.Type == JTokenType.String) - { - string assetPath = token.ToString(); - if (!string.IsNullOrEmpty(assetPath)) - { -#if UNITY_EDITOR // AssetDatabase is editor-only - // Attempt to load the asset from the provided path using the target type - UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath( - assetPath, - targetType - ); - if (loadedAsset != null) - { - return loadedAsset; // Return the loaded asset if successful - } - else - { - // Log a warning if the asset could not be found at the path - Debug.LogWarning( - $"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists."); - return null; - } -#else - Debug.LogWarning($"[ConvertJTokenToType] Asset loading by path ('{assetPath}') is only supported in the Unity Editor."); - return null; -#endif - } - else - { - // Handle cases where an empty string might be intended to clear the reference - return null; // Assign null if the path is empty - } - } - // CASE 3: Reference is null or empty JToken, assign null - else if ( - token.Type == JTokenType.Null - || string.IsNullOrEmpty(token.ToString()) - ) - { - return null; - } - // CASE 4: Invalid format for Unity Object reference - else - { - Debug.LogWarning( - $"[ConvertJTokenToType] Expected a string asset path or a reference object to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}" - ); - return null; - } - } + private static Vector3 ParseJTokenToVector3(JToken token) + { + // ... (implementation - likely replaced by Vector3Converter) ... + // Consider removing these if the serializer handles them reliably. + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z")) + { + return new Vector3(obj["x"].ToObject(), obj["y"].ToObject(), obj["z"].ToObject()); + } + if (token is JArray arr && arr.Count >= 3) + { + return new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Vector3 using fallback. Returning Vector3.zero."); + return Vector3.zero; - // Fallback: Try direct conversion (might work for other simple value types) - // Be cautious here, this might throw errors for complex types not handled above - try - { - return token.ToObject(targetType); - } - catch (Exception directConversionEx) - { - Debug.LogWarning( - $"[ConvertJTokenToType] Direct conversion failed for JToken '{token}' to type '{targetType.Name}': {directConversionEx.Message}. Specific handling might be needed." - ); - return null; - } + } + private static Vector2 ParseJTokenToVector2(JToken token) + { + // ... (implementation - likely replaced by Vector2Converter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) + { + return new Vector2(obj["x"].ToObject(), obj["y"].ToObject()); } - catch (Exception ex) + if (token is JArray arr && arr.Count >= 2) { - Debug.LogWarning( - $"[ConvertJTokenToType] Could not convert JToken '{token}' to type '{targetType.Name}': {ex.Message}" - ); - return null; + return new Vector2(arr[0].ToObject(), arr[1].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Vector2 using fallback. Returning Vector2.zero."); + return Vector2.zero; + } + private static Quaternion ParseJTokenToQuaternion(JToken token) + { + // ... (implementation - likely replaced by QuaternionConverter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && obj.ContainsKey("w")) + { + return new Quaternion(obj["x"].ToObject(), obj["y"].ToObject(), obj["z"].ToObject(), obj["w"].ToObject()); + } + if (token is JArray arr && arr.Count >= 4) + { + return new Quaternion(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Quaternion using fallback. Returning Quaternion.identity."); + return Quaternion.identity; + } + private static Color ParseJTokenToColor(JToken token) + { + // ... (implementation - likely replaced by ColorConverter) ... + if (token is JObject obj && obj.ContainsKey("r") && obj.ContainsKey("g") && obj.ContainsKey("b") && obj.ContainsKey("a")) + { + return new Color(obj["r"].ToObject(), obj["g"].ToObject(), obj["b"].ToObject(), obj["a"].ToObject()); + } + if (token is JArray arr && arr.Count >= 4) + { + return new Color(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Color using fallback. Returning Color.white."); + return Color.white; + } + private static Rect ParseJTokenToRect(JToken token) + { + // ... (implementation - likely replaced by RectConverter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("width") && obj.ContainsKey("height")) + { + return new Rect(obj["x"].ToObject(), obj["y"].ToObject(), obj["width"].ToObject(), obj["height"].ToObject()); } + if (token is JArray arr && arr.Count >= 4) + { + return new Rect(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Rect using fallback. Returning Rect.zero."); + return Rect.zero; + } + private static Bounds ParseJTokenToBounds(JToken token) + { + // ... (implementation - likely replaced by BoundsConverter) ... + if (token is JObject obj && obj.ContainsKey("center") && obj.ContainsKey("size")) + { + // Requires Vector3 conversion, which should ideally use the serializer too + Vector3 center = ParseJTokenToVector3(obj["center"]); // Or use obj["center"].ToObject(inputSerializer) + Vector3 size = ParseJTokenToVector3(obj["size"]); // Or use obj["size"].ToObject(inputSerializer) + return new Bounds(center, size); + } + // Array fallback for Bounds is less intuitive, maybe remove? + // if (token is JArray arr && arr.Count >= 6) + // { + // return new Bounds(new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()), new Vector3(arr[3].ToObject(), arr[4].ToObject(), arr[5].ToObject())); + // } + Debug.LogWarning($"Could not parse JToken '{token}' as Bounds using fallback. Returning new Bounds(Vector3.zero, Vector3.zero)."); + return new Bounds(Vector3.zero, Vector3.zero); } + // --- End Redundant Parse Helpers --- + + /// + /// Finds a specific UnityEngine.Object based on a find instruction JObject. + /// Primarily used by UnityEngineObjectConverter during deserialization. + /// + // Made public static so UnityEngineObjectConverter can call it. Moved from ConvertJTokenToType. + public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Type targetType) + { + string findTerm = instruction["find"]?.ToString(); + string method = instruction["method"]?.ToString()?.ToLower(); + string componentName = instruction["component"]?.ToString(); // Specific component to get + + if (string.IsNullOrEmpty(findTerm)) + { + Debug.LogWarning("Find instruction missing 'find' term."); + return null; + } + + // Use a flexible default search method if none provided + string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; + + // If the target is an asset (Material, Texture, ScriptableObject etc.) try AssetDatabase first + if (typeof(Material).IsAssignableFrom(targetType) || + typeof(Texture).IsAssignableFrom(targetType) || + typeof(ScriptableObject).IsAssignableFrom(targetType) || + targetType.FullName.StartsWith("UnityEngine.U2D") || // Sprites etc. + typeof(AudioClip).IsAssignableFrom(targetType) || + typeof(AnimationClip).IsAssignableFrom(targetType) || + typeof(Font).IsAssignableFrom(targetType) || + typeof(Shader).IsAssignableFrom(targetType) || + typeof(ComputeShader).IsAssignableFrom(targetType) || + typeof(GameObject).IsAssignableFrom(targetType) && findTerm.StartsWith("Assets/")) // Prefab check + { + // Try loading directly by path/GUID first + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(findTerm, targetType); + if (asset != null) return asset; + asset = AssetDatabase.LoadAssetAtPath(findTerm); // Try generic if type specific failed + if (asset != null && targetType.IsAssignableFrom(asset.GetType())) return asset; + + + // If direct path failed, try finding by name/type using FindAssets + string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; // Search by type and name + string[] guids = AssetDatabase.FindAssets(searchFilter); + + if (guids.Length == 1) + { + asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); + if (asset != null) return asset; + } + else if (guids.Length > 1) + { + Debug.LogWarning($"[FindObjectByInstruction] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); + // Optionally return the first one? Or null? Returning null is safer. + return null; + } + // If still not found, fall through to scene search (though unlikely for assets) + } + + + // --- Scene Object Search --- + // Find the GameObject using the internal finder + GameObject foundGo = FindObjectInternal(new JValue(findTerm), searchMethodToUse); + + if (foundGo == null) + { + // Don't warn yet, could still be an asset not found above + // Debug.LogWarning($"Could not find GameObject using instruction: {instruction}"); + return null; + } + + // Now, get the target object/component from the found GameObject + if (targetType == typeof(GameObject)) + { + return foundGo; // We were looking for a GameObject + } + else if (typeof(Component).IsAssignableFrom(targetType)) + { + Type componentToGetType = targetType; + if (!string.IsNullOrEmpty(componentName)) + { + Type specificCompType = FindType(componentName); + if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) + { + componentToGetType = specificCompType; + } + else + { + Debug.LogWarning($"Could not find component type '{componentName}' specified in find instruction. Falling back to target type '{targetType.Name}'."); + } + } + + Component foundComp = foundGo.GetComponent(componentToGetType); + if (foundComp == null) + { + Debug.LogWarning($"Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); + } + return foundComp; + } + else + { + Debug.LogWarning($"Find instruction handling not implemented for target type: {targetType.Name}"); + return null; + } + } + /// /// Helper to find a Type by name, searching relevant assemblies. @@ -2068,37 +1978,50 @@ private static Type FindType(string typeName) if (string.IsNullOrEmpty(typeName)) return null; - // Handle common Unity namespaces implicitly - var type = - Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") - ?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.PhysicsModule") - ?? // Example physics - Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI") - ?? // Example UI - Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule") - ?? Type.GetType(typeName); // Try direct name (if fully qualified or in mscorlib) - - if (type != null) - return type; - - // If not found, search all loaded assemblies (slower) - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - type = assembly.GetType(typeName); - if (type != null) - return type; - // Also check with namespaces if simple name given - type = assembly.GetType("UnityEngine." + typeName); - if (type != null) - return type; - type = assembly.GetType("UnityEditor." + typeName); - if (type != null) - return type; - type = assembly.GetType("UnityEngine.UI." + typeName); - if (type != null) - return type; + // Handle fully qualified names first + Type type = Type.GetType(typeName); + if (type != null) return type; + + // Handle common namespaces implicitly (add more as needed) + string[] namespaces = { "UnityEngine", "UnityEngine.UI", "UnityEngine.AI", "UnityEngine.Animations", "UnityEngine.Audio", "UnityEngine.EventSystems", "UnityEngine.InputSystem", "UnityEngine.Networking", "UnityEngine.Rendering", "UnityEngine.SceneManagement", "UnityEngine.Tilemaps", "UnityEngine.U2D", "UnityEngine.Video", "UnityEditor", "UnityEditor.AI", "UnityEditor.Animations", "UnityEditor.Experimental.GraphView", "UnityEditor.IMGUI.Controls", "UnityEditor.PackageManager.UI", "UnityEditor.SceneManagement", "UnityEditor.UI", "UnityEditor.U2D", "UnityEditor.VersionControl" }; // Add more relevant namespaces + + foreach (string ns in namespaces) { + type = Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}.CoreModule") ?? // Heuristic: Check CoreModule first for UnityEngine/UnityEditor + Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}"); // Try assembly matching namespace root + if (type != null) return type; + } + + + // If not found, search all loaded assemblies (slower, last resort) + // Prioritize assemblies likely to contain game/editor types + Assembly[] priorityAssemblies = { + Assembly.Load("Assembly-CSharp"), // Main game scripts + Assembly.Load("Assembly-CSharp-Editor"), // Main editor scripts + // Add other important project assemblies if known + }; + foreach (var assembly in priorityAssemblies.Where(a => a != null)) + { + type = assembly.GetType(typeName) ?? assembly.GetType("UnityEngine." + typeName) ?? assembly.GetType("UnityEditor." + typeName); + if (type != null) return type; + } + + // Search remaining assemblies + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Except(priorityAssemblies)) + { + try { // Protect against assembly loading errors + type = assembly.GetType(typeName); + if (type != null) return type; + // Also check with common namespaces if simple name given + foreach (string ns in namespaces) { + type = assembly.GetType($"{ns}.{typeName}"); + if (type != null) return type; + } + } catch (Exception ex) { + Debug.LogWarning($"[FindType] Error searching assembly {assembly.FullName}: {ex.Message}"); + } } + Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'"); return null; // Not found } @@ -2111,368 +2034,23 @@ private static Type FindType(string typeName) { try { + // Use ToObject for potentially better handling than direct indexing return new Vector3( array[0].ToObject(), array[1].ToObject(), array[2].ToObject() ); } - catch - { /* Ignore parsing errors */ - } - } - return null; - } - - // --- Data Serialization --- - - /// - /// Creates a serializable representation of a GameObject. - /// - private static object GetGameObjectData(GameObject go) - { - if (go == null) - return null; - return new - { - name = go.name, - instanceID = go.GetInstanceID(), - tag = go.tag, - layer = go.layer, - activeSelf = go.activeSelf, - activeInHierarchy = go.activeInHierarchy, - isStatic = go.isStatic, - scenePath = go.scene.path, // Identify which scene it belongs to - transform = new // Serialize transform components carefully to avoid JSON issues - { - // Serialize Vector3 components individually to prevent self-referencing loops. - // The default serializer can struggle with properties like Vector3.normalized. - position = new - { - x = go.transform.position.x, - y = go.transform.position.y, - z = go.transform.position.z, - }, - localPosition = new - { - x = go.transform.localPosition.x, - y = go.transform.localPosition.y, - z = go.transform.localPosition.z, - }, - rotation = new - { - x = go.transform.rotation.eulerAngles.x, - y = go.transform.rotation.eulerAngles.y, - z = go.transform.rotation.eulerAngles.z, - }, - localRotation = new - { - x = go.transform.localRotation.eulerAngles.x, - y = go.transform.localRotation.eulerAngles.y, - z = go.transform.localRotation.eulerAngles.z, - }, - scale = new - { - x = go.transform.localScale.x, - y = go.transform.localScale.y, - z = go.transform.localScale.z, - }, - forward = new - { - x = go.transform.forward.x, - y = go.transform.forward.y, - z = go.transform.forward.z, - }, - up = new - { - x = go.transform.up.x, - y = go.transform.up.y, - z = go.transform.up.z, - }, - right = new - { - x = go.transform.right.x, - y = go.transform.right.y, - z = go.transform.right.z, - }, - }, - parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent - // Optionally include components, but can be large - // components = go.GetComponents().Select(c => GetComponentData(c)).ToList() - // Or just component names: - componentNames = go.GetComponents() - .Select(c => c.GetType().FullName) - .ToList(), - }; - } - - // --- Metadata Caching for Reflection --- - private class CachedMetadata - { - public readonly List SerializableProperties; - public readonly List SerializableFields; - - public CachedMetadata(List properties, List fields) - { - SerializableProperties = properties; - SerializableFields = fields; - } - } - // Key becomes Tuple - private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>(); - // --- End Metadata Caching --- - - /// - /// Creates a serializable representation of a Component, attempting to serialize - /// public properties and fields using reflection, with caching and control over non-public fields. - /// - // Add the flag parameter here - private static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) - { - if (c == null) return null; - Type componentType = c.GetType(); - - // TEMP: Clear cache for testing again -- REMOVING - // _metadataCache.Clear(); - - var data = new Dictionary - { - { "typeName", componentType.FullName }, - { "instanceID", c.GetInstanceID() } - }; - - // --- Get Cached or Generate Metadata (using new cache key) --- - // _metadataCache.Clear(); // TEMP: Clear cache for testing - REMOVED - Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields); - if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) - { - // ---- ADD THIS ---- - // UnityEngine.Debug.Log($"[MCP Cache Test] Metadata MISS for Type: {componentType.FullName}, IncludeNonPublic: {includeNonPublicSerializedFields}. Generating..."); - // ----------------- - var propertiesToCache = new List(); - var fieldsToCache = new List(); -//test - // Traverse the hierarchy from the component type up to MonoBehaviour - Type currentType = componentType; - while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) - { - // Get properties declared only at the current type level - BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - foreach (var propInfo in currentType.GetProperties(propFlags)) - { - // Basic filtering (readable, not indexer, not transform which is handled elsewhere) - if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; - // Add if not already added (handles overrides - keep the most derived version) - if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) { - propertiesToCache.Add(propInfo); - } - } - - // Get fields declared only at the current type level (both public and non-public) - BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; - var declaredFields = currentType.GetFields(fieldFlags); - - // Process the declared Fields for caching - foreach (var fieldInfo in declaredFields) - { - if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields - - // Add if not already added (handles hiding - keep the most derived version) - if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; - - bool shouldInclude = false; - if (includeNonPublicSerializedFields) - { - // If TRUE, include Public OR NonPublic with [SerializeField] - shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false)); - } - else // includeNonPublicSerializedFields is FALSE - { - // If FALSE, include ONLY if it is explicitly Public. - shouldInclude = fieldInfo.IsPublic; - } - - if (shouldInclude) - { - fieldsToCache.Add(fieldInfo); - } - } - - // Move to the base type - currentType = currentType.BaseType; - } - // --- End Hierarchy Traversal --- - - // REMOVED Original non-hierarchical property/field gathering logic - /* - BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; - BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - foreach (var propInfo in componentType.GetProperties(propFlags)) { ... } - var allQueriedFields = componentType.GetFields(fieldFlags); - foreach (var fieldInfo in allQueriedFields) { ... } - */ - - cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); - _metadataCache[cacheKey] = cachedData; // Add to cache with combined key - } - // ---- ADD THIS ---- - // UnityEngine.Debug.Log($"[MCP Cache Test] Metadata HIT for Type: {componentType.FullName}, IncludeNonPublic: {includeNonPublicSerializedFields}. Using cache."); - // ----------------- - // --- End Get Cached or Generate Metadata --- - - // --- Use cached metadata (no changes needed here) --- - var serializablePropertiesOutput = new Dictionary(); - // Use cached properties - foreach (var propInfo in cachedData.SerializableProperties) - { - // --- Skip known obsolete/problematic Component shortcut properties --- - string propName = propInfo.Name; - if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || - propName == "light" || propName == "animation" || propName == "constantForce" || - propName == "renderer" || propName == "audio" || propName == "networkView" || - propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || - propName == "particleSystem" || - // Also skip potentially problematic Matrix properties prone to cycles/errors - propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") - { - continue; // Skip these properties - } - // --- End Skip --- - - try - { - object value = propInfo.GetValue(c); - // string propName = propInfo.Name; // Moved up - Type propType = propInfo.PropertyType; - AddSerializableValue(serializablePropertiesOutput, propName, propType, value); - } catch (Exception ex) { - Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); + Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}"); } } - - // Use cached fields - foreach (var fieldInfo in cachedData.SerializableFields) - { - try - { - object value = fieldInfo.GetValue(c); - string fieldName = fieldInfo.Name; - Type fieldType = fieldInfo.FieldType; - AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); - } - catch (Exception ex) - { - // Corrected: Use fieldInfo.Name here as fieldName is out of scope - Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); - } - } - // --- End Use cached metadata --- - - if (serializablePropertiesOutput.Count > 0) - { - data["properties"] = serializablePropertiesOutput; - } - - return data; + return null; } - // Helper function to decide how to serialize different types - private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) - { - if (value == null) - { - dict[name] = null; - return; - } - - // Primitives & Enums - if (type.IsPrimitive || type.IsEnum || type == typeof(string)) - { - dict[name] = value; - } - // Known Unity Structs (add more as needed: Rect, Bounds, etc.) - else if (type == typeof(Vector2)) { var v = (Vector2)value; dict[name] = new { v.x, v.y }; } - else if (type == typeof(Vector3)) { var v = (Vector3)value; dict[name] = new { v.x, v.y, v.z }; } - else if (type == typeof(Vector4)) { var v = (Vector4)value; dict[name] = new { v.x, v.y, v.z, v.w }; } - else if (type == typeof(Quaternion)) { var q = (Quaternion)value; dict[name] = new { x = q.eulerAngles.x, y = q.eulerAngles.y, z = q.eulerAngles.z }; } // Serialize as Euler angles for readability - else if (type == typeof(Color)) { var c = (Color)value; dict[name] = new { c.r, c.g, c.b, c.a }; } - // UnityEngine.Object References - else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) - { - var obj = value as UnityEngine.Object; - if (obj != null) { - var refData = new Dictionary { - { "name", obj.name }, - { "instanceID", obj.GetInstanceID() }, - { "typeName", obj.GetType().FullName } - }; - // Attempt to get asset path and GUID -#if UNITY_EDITOR // AssetDatabase is editor-only - string assetPath = AssetDatabase.GetAssetPath(obj); - if (!string.IsNullOrEmpty(assetPath)) { - refData["assetPath"] = assetPath; - // Add GUID if asset path exists - string guid = AssetDatabase.AssetPathToGUID(assetPath); - if (!string.IsNullOrEmpty(guid)) { - refData["guid"] = guid; - } - } -#endif - dict[name] = refData; - - } else { - dict[name] = null; // The object reference is null - } - } - // Add handling for basic Lists/Arrays of primitives? (Example for List) - else if (type == typeof(List)) { - dict[name] = value as List; // Directly serializable - } - // Explicit handling for List - else if (type == typeof(List)) { - var vectorList = value as List; - if (vectorList != null) { - // Serialize each Vector3 into a list of dictionaries - var serializableList = vectorList.Select(v => new Dictionary { - { "x", v.x }, - { "y", v.y }, - { "z", v.z } - }).ToList(); - dict[name] = serializableList; - } else { - dict[name] = null; // Or an empty list, or an error message - } - } - // Attempt to serialize other complex types using JToken - else { - // UnityEngine.Debug.Log($"[MCP Debug] Attempting JToken serialization for field: {name} (Type: {type.FullName})"); // Removed this debug log - try - { - // Let Newtonsoft.Json attempt to serialize the value into a JToken - JToken jValue = JToken.FromObject(value); - // We store the JToken itself; the final JSON serialization will handle it. - // Important: Avoid potential cycles by not serializing excessively deep objects here. - // JToken.FromObject handles basic cycle detection, but complex scenarios might still occur. - // Consider adding depth limits if necessary. - dict[name] = jValue; - } - catch (JsonSerializationException jsonEx) - { - // Handle potential serialization issues (e.g., cycles, unsupported types) - Debug.LogWarning($"[AddSerializableValue] Could not serialize complex type '{type.FullName}' for property '{name}' using JToken: {jsonEx.Message}. Storing skip message."); - dict[name] = $"[Serialization Error: {type.FullName} - {jsonEx.Message}]"; - } - catch (Exception ex) - { - // Catch other unexpected errors during serialization - Debug.LogWarning($"[AddSerializableValue] Unexpected error serializing complex type '{type.FullName}' for property '{name}' using JToken: {ex.Message}"); - dict[name] = $"[Serialization Error: {type.FullName} - Unexpected]"; - } - } - } + // Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup. + // They are now in Helpers.GameObjectSerializer } } From 0433b88f68fd8072da71367e5a0fd0b820008c9b Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 09:45:56 -0700 Subject: [PATCH 4/7] Fix: Add missing .meta file for GameObjectSerializer --- UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta new file mode 100644 index 00000000..d8df9686 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 64b8ff807bc9a401c82015cbafccffac \ No newline at end of file From bd850729c6bc60a704b0d1374ef1c60cc39a4cc8 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 10:07:20 -0700 Subject: [PATCH 5/7] Fix: Add missing Runtime assembly and related files --- .../Editor/UnityMcpBridge.Editor.asmdef | 19 ++ .../Editor/UnityMcpBridge.Editor.asmdef.meta | 7 + UnityMcpBridge/Runtime.meta | 8 + UnityMcpBridge/Runtime/Serialization.meta | 8 + .../Serialization/UnityTypeConverters.cs | 266 ++++++++++++++++++ .../Serialization/UnityTypeConverters.cs.meta | 2 + .../Runtime/UnityMcpBridge.Runtime.asmdef | 16 ++ .../UnityMcpBridge.Runtime.asmdef.meta | 7 + 8 files changed, 333 insertions(+) create mode 100644 UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef create mode 100644 UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef.meta create mode 100644 UnityMcpBridge/Runtime.meta create mode 100644 UnityMcpBridge/Runtime/Serialization.meta create mode 100644 UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs create mode 100644 UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta create mode 100644 UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef create mode 100644 UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef.meta diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef b/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef new file mode 100644 index 00000000..b006d0af --- /dev/null +++ b/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "UnityMcpBridge.Editor", + "rootNamespace": "UnityMcpBridge.Editor", + "references": [ + "UnityMcpBridge.Runtime", + "GUID:560b04d1a97f54a46a2660c3cc343a6f" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef.meta b/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef.meta new file mode 100644 index 00000000..7f200d1f --- /dev/null +++ b/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 04b0581466993404a8fae14802c2a5a6 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Runtime.meta b/UnityMcpBridge/Runtime.meta new file mode 100644 index 00000000..ae1e4dfa --- /dev/null +++ b/UnityMcpBridge/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5cc10fd969474b3680332e542416860 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Runtime/Serialization.meta b/UnityMcpBridge/Runtime/Serialization.meta new file mode 100644 index 00000000..89cd67ad --- /dev/null +++ b/UnityMcpBridge/Runtime/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7e33d6224fe6473f9bc69fe6d40e508 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs new file mode 100644 index 00000000..ad8f150e --- /dev/null +++ b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs @@ -0,0 +1,266 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; // Required for AssetDatabase and EditorUtility +#endif + +namespace UnityMcpBridge.Runtime.Serialization +{ + public class Vector3Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("z"); + writer.WriteValue(value.z); + writer.WriteEndObject(); + } + + public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Vector3( + (float)jo["x"], + (float)jo["y"], + (float)jo["z"] + ); + } + } + + public class Vector2Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WriteEndObject(); + } + + public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Vector2( + (float)jo["x"], + (float)jo["y"] + ); + } + } + + public class QuaternionConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("z"); + writer.WriteValue(value.z); + writer.WritePropertyName("w"); + writer.WriteValue(value.w); + writer.WriteEndObject(); + } + + public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Quaternion( + (float)jo["x"], + (float)jo["y"], + (float)jo["z"], + (float)jo["w"] + ); + } + } + + public class ColorConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("r"); + writer.WriteValue(value.r); + writer.WritePropertyName("g"); + writer.WriteValue(value.g); + writer.WritePropertyName("b"); + writer.WriteValue(value.b); + writer.WritePropertyName("a"); + writer.WriteValue(value.a); + writer.WriteEndObject(); + } + + public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Color( + (float)jo["r"], + (float)jo["g"], + (float)jo["b"], + (float)jo["a"] + ); + } + } + + public class RectConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("width"); + writer.WriteValue(value.width); + writer.WritePropertyName("height"); + writer.WriteValue(value.height); + writer.WriteEndObject(); + } + + public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Rect( + (float)jo["x"], + (float)jo["y"], + (float)jo["width"], + (float)jo["height"] + ); + } + } + + public class BoundsConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("center"); + serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3 + writer.WritePropertyName("size"); + serializer.Serialize(writer, value.size); // Use serializer to handle nested Vector3 + writer.WriteEndObject(); + } + + public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + Vector3 center = jo["center"].ToObject(serializer); // Use serializer to handle nested Vector3 + Vector3 size = jo["size"].ToObject(serializer); // Use serializer to handle nested Vector3 + return new Bounds(center, size); + } + } + + // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.) + public class UnityEngineObjectConverter : JsonConverter + { + public override bool CanRead => true; // We need to implement ReadJson + public override bool CanWrite => true; + + public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + +#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only + if (UnityEditor.AssetDatabase.Contains(value)) + { + // It's an asset (Material, Texture, Prefab, etc.) + string path = UnityEditor.AssetDatabase.GetAssetPath(value); + if (!string.IsNullOrEmpty(path)) + { + writer.WriteValue(path); + } + else + { + // Asset exists but path couldn't be found? Write minimal info. + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WritePropertyName("isAssetWithoutPath"); + writer.WriteValue(true); + writer.WriteEndObject(); + } + } + else + { + // It's a scene object (GameObject, Component, etc.) + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WriteEndObject(); + } +#else + // Runtime fallback: Write basic info without AssetDatabase + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WritePropertyName("warning"); + writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable."); + writer.WriteEndObject(); +#endif + } + + public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + +#if UNITY_EDITOR + if (reader.TokenType == JsonToken.String) + { + // Assume it's an asset path + string path = reader.Value.ToString(); + return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType); + } + + if (reader.TokenType == JsonToken.StartObject) + { + JObject jo = JObject.Load(reader); + if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer) + { + int instanceId = idToken.ToObject(); + UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId); + if (obj != null && objectType.IsAssignableFrom(obj.GetType())) + { + return obj; + } + } + // Could potentially try finding by name as a fallback if ID lookup fails/isn't present + // but that's less reliable. + } +#else + // Runtime deserialization is tricky without AssetDatabase/EditorUtility + // Maybe log a warning and return null or existingValue? + 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 + return existingValue; +#endif + + throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object"); + } + } +} \ No newline at end of file diff --git a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta new file mode 100644 index 00000000..9596160f --- /dev/null +++ b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e65311c160f0d41d4a1b45a3dba8dd5a \ No newline at end of file diff --git a/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef b/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef new file mode 100644 index 00000000..3e8a8d9c --- /dev/null +++ b/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "UnityMcpBridge.Runtime", + "rootNamespace": "UnityMcpBridge.Runtime", + "references": [ + "GUID:560b04d1a97f54a46a2660c3cc343a6f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef.meta b/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef.meta new file mode 100644 index 00000000..538a2573 --- /dev/null +++ b/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7d76fa93cbc5144028727fd2dbac5655 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 51eb59f04fa70c39d4e574282511d32503d70652 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 12:52:18 -0700 Subject: [PATCH 6/7] Fix(MCP): Resolve ValidTRS crash during component serialization The Unity Editor was crashing with ValidTRS() assertions when attempting to get components from certain GameObjects like the Main Camera. Investigation revealed the crash occurred during JSON serialization when reflection code accessed specific matrix properties (e.g., Camera.cullingMatrix, Transform.rotation, Transform.lossyScale). Accessing these properties appears to trigger internal Transform state validation failures, potentially due to interactions with the JSON serializer's reflection mechanism. This fix addresses the issue by: - Replacing LINQ iteration in GetComponentsFromTarget with a standard loop over a copied list to prevent potential premature serialization interactions. - Explicitly skipping known problematic Camera matrix properties (cullingMatrix, pixelRect, rect) and generic matrix properties (worldToLocalMatrix, localToWorldMatrix) within GetComponentData's reflection logic. - Retaining manual serialization for Transform component properties to avoid related reflection issues. --- .../Editor/Helpers/GameObjectSerializer.cs | 96 +++++++++++++++++-- .../Editor/Tools/ManageGameObject.cs | 53 +++++++++- 2 files changed, 135 insertions(+), 14 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs index 8a65ada3..1a1b462b 100644 --- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -13,7 +13,7 @@ namespace UnityMcpBridge.Editor.Helpers /// /// Handles serialization of GameObjects and Components for MCP responses. /// Includes reflection helpers and caching for performance. - /// + /// tew public static class GameObjectSerializer { // --- Data Serialization --- @@ -121,9 +121,42 @@ public CachedMetadata(List properties, List fields) // Add the flag parameter here public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) { + // --- Add Early Logging --- + // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); + // --- End Early Logging --- + if (c == null) return null; Type componentType = c.GetType(); + // --- Special handling for Transform to avoid reflection crashes and problematic properties --- + if (componentType == typeof(Transform)) + { + Transform tr = c as Transform; + // Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})"); + return new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", tr.GetInstanceID() }, + // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'. + { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, // Use Euler angles + { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 }, + { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, + { "childCount", tr.childCount }, + // Include standard Object/Component properties + { "name", tr.name }, + { "tag", tr.tag }, + { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } + }; + } + // --- End Special handling for Transform --- + var data = new Dictionary { { "typeName", componentType.FullName }, @@ -195,11 +228,18 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // --- Use cached metadata --- var serializablePropertiesOutput = new Dictionary(); + + // --- Add Logging Before Property Loop --- + // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}..."); + // --- End Logging Before Property Loop --- + // Use cached properties foreach (var propInfo in cachedData.SerializableProperties) { - // --- Skip known obsolete/problematic Component shortcut properties --- string propName = propInfo.Name; + + // --- Skip known obsolete/problematic Component shortcut properties --- + bool skipProperty = false; if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || propName == "light" || propName == "animation" || propName == "constantForce" || propName == "renderer" || propName == "audio" || propName == "networkView" || @@ -208,27 +248,63 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // Also skip potentially problematic Matrix properties prone to cycles/errors propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") { - continue; // Skip these properties + // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log + skipProperty = true; + } + // --- End Skip Generic Properties --- + + // --- Skip specific potentially problematic Camera properties --- + if (componentType == typeof(Camera) && + (propName == "pixelRect" || + propName == "rect" || + propName == "cullingMatrix")) + { + // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}"); + skipProperty = true; + } + // --- End Skip Camera Properties --- + + // --- Skip specific potentially problematic Transform properties --- + if (componentType == typeof(Transform) && (propName == "lossyScale" || propName == "rotation")) + { + // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); + skipProperty = true; + } + // --- End Skip Transform Properties --- + + // Skip if flagged + if (skipProperty) + { + continue; } - // --- End Skip --- try { + // --- Add detailed logging --- + // Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}"); + // --- End detailed logging --- object value = propInfo.GetValue(c); Type propType = propInfo.PropertyType; AddSerializableValue(serializablePropertiesOutput, propName, propType, value); } catch (Exception ex) { - Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); + // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); } } + // --- Add Logging Before Field Loop --- + // Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}..."); + // --- End Logging Before Field Loop --- + // Use cached fields foreach (var fieldInfo in cachedData.SerializableFields) { try { + // --- Add detailed logging for fields --- + // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); + // --- End detailed logging for fields --- object value = fieldInfo.GetValue(c); string fieldName = fieldInfo.Name; Type fieldType = fieldInfo.FieldType; @@ -236,7 +312,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ } catch (Exception ex) { - Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); + // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); } } // --- End Use cached metadata --- @@ -273,7 +349,7 @@ private static void AddSerializableValue(Dictionary dict, string catch (Exception e) { // Catch potential errors during JToken conversion or addition to dictionary - Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); + // Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); } } @@ -329,7 +405,7 @@ private static object ConvertJTokenToPlainObject(JToken token) { return jValue.Value; } - Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); + // Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); return null; } } @@ -365,12 +441,12 @@ private static JToken CreateTokenFromValue(object value, Type type) } catch (JsonSerializationException e) { - Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); + // Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); return null; // Indicate serialization failure } catch (Exception e) // Catch other unexpected errors { - Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); + // Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); return null; // Indicate serialization failure } } diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 1e22ebd9..66c64cb9 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -837,12 +837,57 @@ private static object GetComponentsFromTarget(string target, string searchMethod try { - Component[] components = targetGo.GetComponents(); - // Use the new serializer helper and pass the flag - var componentData = components.Select(c => Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized)).ToList(); + // --- Get components, immediately copy to list, and null original array --- + Component[] originalComponents = targetGo.GetComponents(); + List componentsToIterate = new List(originalComponents ?? Array.Empty()); // Copy immediately, handle null case + int componentCount = componentsToIterate.Count; + originalComponents = null; // Null the original reference + // Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop..."); + // --- End Copy and Null --- + + var componentData = new List(); + + for (int i = componentCount - 1; i >= 0; i--) // Iterate backwards over the COPY + { + Component c = componentsToIterate[i]; // Use the copy + if (c == null) + { + // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] Encountered a null component at index {i} on {targetGo.name}. Skipping."); + continue; // Safety check + } + // Debug.Log($"[GetComponentsFromTarget REVERSE for] Processing component: {c.GetType()?.FullName ?? "null"} (ID: {c.GetInstanceID()}) at index {i} on {targetGo.name}"); + try + { + var data = Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized); + if (data != null) // Ensure GetComponentData didn't return null + { + componentData.Insert(0, data); // Insert at beginning to maintain original order in final list + } + // else + // { + // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] GetComponentData returned null for component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}. Skipping addition."); + // } + } + catch (Exception ex) + { + Debug.LogError($"[GetComponentsFromTarget REVERSE for] Error processing component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}: {ex.Message}\n{ex.StackTrace}"); + // Optionally add placeholder data or just skip + componentData.Insert(0, new JObject( // Insert error marker at beginning + new JProperty("typeName", c.GetType().FullName + " (Serialization Error)"), + new JProperty("instanceID", c.GetInstanceID()), + new JProperty("error", ex.Message) + )); + } + } + // Debug.Log($"[GetComponentsFromTarget] Finished REVERSE for loop."); + + // Cleanup the list we created + componentsToIterate.Clear(); + componentsToIterate = null; + return Response.Success( $"Retrieved {componentData.Count} components from '{targetGo.name}'.", - componentData + componentData // List was built in original order ); } catch (Exception e) From 3063d5407171c3ff1ef1da9e17f4f97072197d81 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 10 Apr 2025 13:10:10 -0700 Subject: [PATCH 7/7] fix: Improved component data retrieval with special handling for Transform and Camera components to prevent serialization crashes --- .../Editor/Helpers/GameObjectSerializer.cs | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs index 1a1b462b..51f6d97c 100644 --- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -157,6 +157,69 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ } // --- End Special handling for Transform --- + // --- Special handling for Camera to avoid matrix-related crashes --- + if (componentType == typeof(Camera)) + { + Camera cam = c as Camera; + var cameraProperties = new Dictionary(); + + // List of safe properties to serialize + var safeProperties = new Dictionary> + { + { "nearClipPlane", () => cam.nearClipPlane }, + { "farClipPlane", () => cam.farClipPlane }, + { "fieldOfView", () => cam.fieldOfView }, + { "renderingPath", () => (int)cam.renderingPath }, + { "actualRenderingPath", () => (int)cam.actualRenderingPath }, + { "allowHDR", () => cam.allowHDR }, + { "allowMSAA", () => cam.allowMSAA }, + { "allowDynamicResolution", () => cam.allowDynamicResolution }, + { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture }, + { "orthographicSize", () => cam.orthographicSize }, + { "orthographic", () => cam.orthographic }, + { "opaqueSortMode", () => (int)cam.opaqueSortMode }, + { "transparencySortMode", () => (int)cam.transparencySortMode }, + { "depth", () => cam.depth }, + { "aspect", () => cam.aspect }, + { "cullingMask", () => cam.cullingMask }, + { "eventMask", () => cam.eventMask }, + { "backgroundColor", () => cam.backgroundColor }, + { "clearFlags", () => (int)cam.clearFlags }, + { "stereoEnabled", () => cam.stereoEnabled }, + { "stereoSeparation", () => cam.stereoSeparation }, + { "stereoConvergence", () => cam.stereoConvergence }, + { "enabled", () => cam.enabled }, + { "name", () => cam.name }, + { "tag", () => cam.tag }, + { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } } + }; + + foreach (var prop in safeProperties) + { + try + { + var value = prop.Value(); + if (value != null) + { + AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value); + } + } + catch (Exception) + { + // Silently skip any property that fails + continue; + } + } + + return new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", cam.GetInstanceID() }, + { "properties", cameraProperties } + }; + } + // --- End Special handling for Camera --- + var data = new Dictionary { { "typeName", componentType.FullName }, @@ -257,7 +320,13 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ if (componentType == typeof(Camera) && (propName == "pixelRect" || propName == "rect" || - propName == "cullingMatrix")) + propName == "cullingMatrix" || + propName == "useOcclusionCulling" || + propName == "worldToCameraMatrix" || + propName == "projectionMatrix" || + propName == "nonJitteredProjectionMatrix" || + propName == "previousViewProjectionMatrix" || + propName == "cameraToWorldMatrix")) { // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}"); skipProperty = true; @@ -265,7 +334,11 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // --- End Skip Camera Properties --- // --- Skip specific potentially problematic Transform properties --- - if (componentType == typeof(Transform) && (propName == "lossyScale" || propName == "rotation")) + if (componentType == typeof(Transform) && + (propName == "lossyScale" || + propName == "rotation" || + propName == "worldToLocalMatrix" || + propName == "localToWorldMatrix")) { // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); skipProperty = true;