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