diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs new file mode 100644 index 0000000..5fc3fce --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -0,0 +1,527 @@ +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) + { + // --- 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 --- + + // --- 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 }, + { "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(); + + // --- 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) + { + 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" || + propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || + propName == "particleSystem" || + // Also skip potentially problematic Matrix properties prone to cycles/errors + propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") + { + // 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" || + propName == "useOcclusionCulling" || + propName == "worldToCameraMatrix" || + propName == "projectionMatrix" || + propName == "nonJitteredProjectionMatrix" || + propName == "previousViewProjectionMatrix" || + propName == "cameraToWorldMatrix")) + { + // 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" || + propName == "worldToLocalMatrix" || + propName == "localToWorldMatrix")) + { + // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); + skipProperty = true; + } + // --- End Skip Transform Properties --- + + // Skip if flagged + if (skipProperty) + { + continue; + } + + 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}"); + } + } + + // --- 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; + 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/Helpers/GameObjectSerializer.cs.meta b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta new file mode 100644 index 0000000..d8df968 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 64b8ff807bc9a401c82015cbafccffac \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index bf19161..36897a9 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -2,6 +2,7 @@ using System; 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; @@ -22,6 +23,7 @@ namespace UnityMcpBridge.Editor.Tools public static object HandleCommand(JObject @params) { + string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) { @@ -38,6 +40,10 @@ namespace UnityMcpBridge.Editor.Tools string layer = @params["layer"]?.ToString(); JToken parentToken = @params["parent"]; + // --- Add parameter for controlling non-public field inclusion --- + bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject() ?? true; // Default to true + // --- End add parameter --- + // --- Prefab Redirection Check --- string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; @@ -126,7 +132,8 @@ namespace UnityMcpBridge.Editor.Tools 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": @@ -204,15 +211,12 @@ namespace UnityMcpBridge.Editor.Tools 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); @@ -234,13 +238,11 @@ namespace UnityMcpBridge.Editor.Tools $"Failed to instantiate prefab at '{prefabPath}'." ); } - // 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, @@ -312,14 +314,12 @@ namespace UnityMcpBridge.Editor.Tools newGo = new GameObject(name); 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) { @@ -475,13 +475,6 @@ namespace UnityMcpBridge.Editor.Tools 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 @@ -497,7 +490,6 @@ namespace UnityMcpBridge.Editor.Tools $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" ); } - // Use SaveAsPrefabAssetAndConnect with the final saving path finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( newGo, @@ -555,8 +547,9 @@ namespace UnityMcpBridge.Editor.Tools $"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, GetGameObjectData(finalInstance)); + return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); } private static object ModifyGameObject( @@ -592,6 +585,7 @@ namespace UnityMcpBridge.Editor.Tools if (parentToken != null) { GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); + // Check for hierarchy loops if ( newParentGo == null && !( @@ -605,7 +599,6 @@ namespace UnityMcpBridge.Editor.Tools { return Response.Error($"New parent ('{parentToken}') not found."); } - // Check for hierarchy loops if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) { return Response.Error( @@ -635,10 +628,8 @@ namespace UnityMcpBridge.Editor.Tools { // 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; } @@ -659,7 +650,7 @@ namespace UnityMcpBridge.Editor.Tools // 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." ); @@ -730,6 +721,7 @@ namespace UnityMcpBridge.Editor.Tools { foreach (var compToken in componentsToRemoveArray) { + // ... (parsing logic as in CreateGameObject) ... string typeName = compToken.ToString(); if (!string.IsNullOrEmpty(typeName)) { @@ -746,7 +738,6 @@ namespace UnityMcpBridge.Editor.Tools { foreach (var compToken in componentsToAddArrayModify) { - // ... (parsing logic as in CreateGameObject) ... string typeName = null; JObject properties = null; if (compToken.Type == JTokenType.String) @@ -790,17 +781,27 @@ namespace UnityMcpBridge.Editor.Tools if (!modified) { + // Use the new serializer helper + // return Response.Success( + // $"No modifications applied to GameObject '{targetGo.name}'.", + // GetGameObjectData(targetGo)); + 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) ); + // return Response.Success( + // $"GameObject '{targetGo.name}' modified successfully.", + // GetGameObjectData(targetGo)); + } private static object DeleteGameObject(JToken targetToken, string searchMethod) @@ -862,11 +863,13 @@ namespace UnityMcpBridge.Editor.Tools 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 => GetGameObjectData(go)).ToList(); + var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList(); 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 = true) { GameObject targetGo = FindObjectInternal(target, searchMethod); if (targetGo == null) @@ -878,11 +881,57 @@ namespace UnityMcpBridge.Editor.Tools try { - Component[] components = targetGo.GetComponents(); - var componentData = components.Select(c => GetComponentData(c)).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) @@ -943,9 +992,10 @@ namespace UnityMcpBridge.Editor.Tools 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 } @@ -989,9 +1039,10 @@ namespace UnityMcpBridge.Editor.Tools 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) ); } @@ -1037,9 +1088,10 @@ namespace UnityMcpBridge.Editor.Tools 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) ); } @@ -1316,11 +1368,6 @@ namespace UnityMcpBridge.Editor.Tools } } - // 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 @@ -1466,43 +1513,72 @@ namespace UnityMcpBridge.Editor.Tools 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; @@ -1511,7 +1587,9 @@ namespace UnityMcpBridge.Editor.Tools /// /// 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 + //Using the serializer helper + private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) { try { @@ -1550,7 +1628,6 @@ namespace UnityMcpBridge.Editor.Tools } } } - // Get the property/field PropertyInfo propInfo = currentType.GetProperty(part, flags); FieldInfo fieldInfo = null; @@ -1571,8 +1648,7 @@ namespace UnityMcpBridge.Editor.Tools propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject); - - // If the current property is null, we need to stop + //Need to stop if current property is null if (currentObject == null) { Debug.LogWarning( @@ -1580,8 +1656,7 @@ namespace UnityMcpBridge.Editor.Tools ); return false; } - - // If this is an array/list access, get the element at the index + // If this part was an array or list, access the specific index if (isArray) { if (currentObject is Material[]) @@ -1616,8 +1691,6 @@ namespace UnityMcpBridge.Editor.Tools return false; } } - - // Update type for next iteration currentType = currentObject.GetType(); } @@ -1627,94 +1700,41 @@ namespace UnityMcpBridge.Editor.Tools // 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) - { + // Try converting to Texture using the serializer/converter + try { + Texture texture = value.ToObject(inputSerializer); + 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; - } + } 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; } @@ -1723,32 +1743,37 @@ namespace UnityMcpBridge.Editor.Tools 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}'" ); } } @@ -1756,13 +1781,14 @@ namespace UnityMcpBridge.Editor.Tools 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 /// @@ -1792,250 +1818,257 @@ namespace UnityMcpBridge.Editor.Tools 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) { + if (token == null || token.Type == JTokenType.Null) + { + if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) + { + Debug.LogWarning($"Cannot assign null to non-nullable value type {targetType.Name}. Returning default value."); + return Activator.CreateInstance(targetType); + } + return null; + } + try { - // Unwrap nested material properties if we're assigning to a Material - if (typeof(Material).IsAssignableFrom(targetType) && token is JObject materialProps) - { - // Handle case where we're passing shader properties directly in a nested object - string materialPath = token["path"]?.ToString(); - if (!string.IsNullOrEmpty(materialPath)) - { - // 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; - } - } - - // If no path is specified, could be a dynamic material or instance set by reference - 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; - } - - // 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; - } - - 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)) - { - // 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 - { - // 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; - } - } - - // 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; - } + // 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.LogWarning( - $"[ConvertJTokenToType] Could not convert JToken '{token}' to type '{targetType.Name}': {ex.Message}" - ); - return null; + 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; } + // --- ParseJTokenTo... helpers are likely redundant now with the serializer approach --- + // Keep them temporarily for reference or if specific fallback logic is ever needed. + + 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; + + } + 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()); + } + if (token is JArray arr && arr.Count >= 2) + { + 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. /// @@ -2044,37 +2077,50 @@ namespace UnityMcpBridge.Editor.Tools 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) + // Handle fully qualified names first + Type type = Type.GetType(typeName); + if (type != null) return type; - 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 - // If not found, search all loaded assemblies (slower) - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + 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)) { - 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; + 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 } @@ -2087,135 +2133,23 @@ namespace UnityMcpBridge.Editor.Tools { 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 */ + catch (Exception ex) + { + Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}"); } } 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(), - }; - } - - /// - /// Creates a serializable representation of a Component. - /// TODO: Add property serialization. - /// - private static object GetComponentData(Component c) - { - if (c == null) - return null; - var data = new Dictionary - { - { "typeName", c.GetType().FullName }, - { "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 { } - } - foreach (var field in type.GetFields(flags)) { - try { properties[field.Name] = field.GetValue(c); } catch { } - } - data["properties"] = properties; - } catch (Exception ex) { - data["propertiesError"] = ex.Message; - } - */ - return data; - } + // Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup. + // They are now in Helpers.GameObjectSerializer } } diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef b/UnityMcpBridge/Editor/UnityMcpBridge.Editor.asmdef new file mode 100644 index 0000000..b006d0a --- /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 0000000..7f200d1 --- /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/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index e6b46db..2242cd6 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -267,6 +267,7 @@ namespace UnityMcpBridge.Editor // Normal JSON command processing Command command = JsonConvert.DeserializeObject(commandText); + if (command == null) { var nullCommandResponse = new diff --git a/UnityMcpBridge/Runtime.meta b/UnityMcpBridge/Runtime.meta new file mode 100644 index 0000000..ae1e4df --- /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 0000000..89cd67a --- /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 0000000..ad8f150 --- /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 0000000..9596160 --- /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 0000000..3e8a8d9 --- /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 0000000..538a257 --- /dev/null +++ b/UnityMcpBridge/Runtime/UnityMcpBridge.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7d76fa93cbc5144028727fd2dbac5655 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpServer/src/tools/manage_gameobject.py b/UnityMcpServer/src/tools/manage_gameobject.py index a65331f..83ab9c7 100644 --- a/UnityMcpServer/src/tools/manage_gameobject.py +++ b/UnityMcpServer/src/tools/manage_gameobject.py @@ -35,11 +35,12 @@ def register_manage_gameobject_tools(mcp: FastMCP): 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. Args: - action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property'). + action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components'). target: GameObject identifier (name or path string) for modify/delete/component actions. search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. name: GameObject name - used for both 'create' (initial name) and 'modify' (rename). @@ -59,9 +60,18 @@ def register_manage_gameobject_tools(mcp: FastMCP): 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. + + Action-specific details: + - For 'get_components': + Required: target, search_method + Optional: includeNonPublicSerialized (defaults to True) + Returns all components on the target GameObject with their serialized data. + The search_method parameter determines how to find the target ('by_name', 'by_id', 'by_path'). Returns: Dictionary with operation results ('success', 'message', 'data'). + For 'get_components', the 'data' field contains a dictionary of component names and their serialized properties. """ try: # --- Early check for attempting to modify a prefab asset --- @@ -91,7 +101,8 @@ def register_manage_gameobject_tools(mcp: FastMCP): "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 2f8a4d5..bc3e54c 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" },