using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Runtime.Serialization; // For Converters
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.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.GetInstanceIDCompat(),
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.GetInstanceIDCompat() ?? 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 ---
///
/// Checks if a type is or derives from a type with the specified full name.
/// Used to detect special-case components including their subclasses.
///
private static bool IsOrDerivedFrom(Type type, string baseTypeFullName)
{
Type current = type;
while (current != null)
{
if (current.FullName == baseTypeFullName)
return true;
current = current.BaseType;
}
return false;
}
// Type full names that are known to crash the Editor when accessed via reflection.
// Photon Fusion uses IL weaving to inject fields with these types into NetworkBehaviour
// subclasses. They contain native/unmanaged memory and cannot be safely serialized.
private static readonly HashSet _crashingTypeNames = new HashSet
{
"Fusion.NetworkBehaviourBuffer",
"Fusion.NetworkBehaviourCallbackBuffer",
"Fusion.Networked+Internals",
"Fusion.Changed`1",
};
private static readonly PropertyInfo _isByRefLikeProperty = typeof(Type).GetProperty("IsByRefLike");
///
/// Checks if a type is unsafe to access via reflection or serialize.
/// Returns true for ref structs (Span, ReadOnlySpan), pointer types,
/// by-ref types, and known IL-weaved types that crash the Editor.
///
private static bool IsUnsafeType(Type type)
{
return IsUnsafeType(type, new HashSet());
}
private static bool IsUnsafeType(Type type, HashSet visitedTypes)
{
if (type == null) return false;
if (!visitedTypes.Add(type)) return false;
// Pointer and by-ref types cannot be serialized
if (type.IsPointer || type.IsByRef)
return true;
// Ref structs (Span<>, ReadOnlySpan<>, etc.) cannot be boxed. Use reflection
// so Unity versions without Type.IsByRefLike still compile.
if (type.IsValueType && _isByRefLikeProperty != null && (bool)_isByRefLikeProperty.GetValue(type, null))
return true;
// Check the type and its generic definition against the blacklist
string fullName = type.FullName;
if (fullName != null && _crashingTypeNames.Contains(fullName))
return true;
if (type.IsGenericType)
{
string genericFullName = type.GetGenericTypeDefinition()?.FullName;
if (genericFullName != null && _crashingTypeNames.Contains(genericFullName))
return true;
}
// Catch-all for Fusion buffer types injected by IL weaving
if (fullName != null && fullName.StartsWith("Fusion.") && fullName.Contains("Buffer"))
return true;
// Arrays and generic containers can wrap unsafe Fusion/ref-like types.
// Newtonsoft.Json would still recurse into those values during serialization.
Type elementType = type.GetElementType();
if (elementType != null && IsUnsafeType(elementType, visitedTypes))
return true;
foreach (Type genericArgument in type.GetGenericArguments())
{
if (IsUnsafeType(genericArgument, visitedTypes))
return true;
}
return false;
}
///
/// Serializes a UnityEngine.Object reference to a dictionary with name, instanceID, and assetPath.
/// Used for consistent serialization of asset references in special-case component handlers.
///
/// The Unity object to serialize
/// Whether to include the asset path (default true)
/// A dictionary with the object's reference info, or null if obj is null
private static Dictionary SerializeAssetReference(UnityEngine.Object obj, bool includeAssetPath = true)
{
if (obj == null) return null;
var result = new Dictionary
{
{ "name", obj.name },
{ "instanceID", obj.GetInstanceIDCompat() }
};
if (includeAssetPath)
{
var assetPath = AssetDatabase.GetAssetPath(obj);
result["assetPath"] = string.IsNullOrEmpty(assetPath) ? null : assetPath;
}
return result;
}
///
/// 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 ---
// McpLog.Info($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceIDCompat() ?? 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;
// McpLog.Info($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceIDCompat()})");
return new Dictionary
{
{ "typeName", componentType.FullName },
{ "instanceID", tr.GetInstanceIDCompat() },
// Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
{ "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject