using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using System.Collections.Generic;
#endif
namespace LocalMcp.UnityServer
{
#if UNITY_EDITOR
/// <summary>
/// Provides SerializedObject inspection capabilities for detecting missing references
/// </summary>
public static class SerializedObjectInspector
{
/// <summary>
/// Inspects a component's SerializedObject and returns detailed property information
/// including object references and missing reference detection
/// </summary>
public static string InspectSerializedObject(string paramsJson, object id)
{
try
{
string gameObjectPath = ExtractStringParam(paramsJson, "gameObjectPath");
string componentType = ExtractStringParam(paramsJson, "componentType");
if (string.IsNullOrEmpty(gameObjectPath) || string.IsNullOrEmpty(componentType))
{
return JsonRpcResponseHelper.ErrorMessage("gameObjectPath and componentType are required", id);
}
// Find GameObject
GameObject target = GameObject.Find(gameObjectPath);
if (target == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject not found: {gameObjectPath}", id);
}
// Find component
Component component = target.GetComponent(componentType);
if (component == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component not found: {componentType} on {gameObjectPath}", id);
}
// Create SerializedObject
SerializedObject serializedObject = new SerializedObject(component);
SerializedProperty property = serializedObject.GetIterator();
var properties = new List<string>();
bool enterChildren = true;
while (property.NextVisible(enterChildren))
{
enterChildren = false; // Only enter children on first iteration
var propertyInfo = new Dictionary<string, object>
{
{ "path", property.propertyPath },
{ "name", property.name },
{ "displayName", property.displayName },
{ "propertyType", property.propertyType.ToString() },
{ "isArray", property.isArray },
{ "arraySize", property.isArray ? property.arraySize : 0 },
{ "depth", property.depth }
};
// Add type-specific information
switch (property.propertyType)
{
case SerializedPropertyType.ObjectReference:
propertyInfo["objectReferenceValue"] = property.objectReferenceValue != null ? property.objectReferenceValue.name : "null";
propertyInfo["objectReferenceInstanceIDValue"] = property.objectReferenceInstanceIDValue;
// Detect missing reference: null value but non-zero instance ID
propertyInfo["isMissing"] = property.objectReferenceValue == null && property.objectReferenceInstanceIDValue != 0;
propertyInfo["objectType"] = property.objectReferenceValue != null ? property.objectReferenceValue.GetType().Name : "null";
break;
case SerializedPropertyType.Integer:
propertyInfo["intValue"] = property.intValue;
break;
case SerializedPropertyType.Boolean:
propertyInfo["boolValue"] = property.boolValue;
break;
case SerializedPropertyType.Float:
propertyInfo["floatValue"] = property.floatValue;
break;
case SerializedPropertyType.String:
propertyInfo["stringValue"] = property.stringValue;
break;
case SerializedPropertyType.Color:
var color = property.colorValue;
propertyInfo["colorValue"] = $"({color.r:F2}, {color.g:F2}, {color.b:F2}, {color.a:F2})";
break;
case SerializedPropertyType.Vector2:
var v2 = property.vector2Value;
propertyInfo["vector2Value"] = $"({v2.x:F2}, {v2.y:F2})";
break;
case SerializedPropertyType.Vector3:
var v3 = property.vector3Value;
propertyInfo["vector3Value"] = $"({v3.x:F2}, {v3.y:F2}, {v3.z:F2})";
break;
case SerializedPropertyType.Vector4:
var v4 = property.vector4Value;
propertyInfo["vector4Value"] = $"({v4.x:F2}, {v4.y:F2}, {v4.z:F2}, {v4.w:F2})";
break;
case SerializedPropertyType.Enum:
propertyInfo["enumValueIndex"] = property.enumValueIndex;
propertyInfo["enumDisplayNames"] = string.Join(", ", property.enumDisplayNames);
break;
}
propertyInfo["tooltip"] = property.tooltip;
propertyInfo["hasMultipleDifferentValues"] = property.hasMultipleDifferentValues;
propertyInfo["editable"] = property.editable;
properties.Add(JsonHelper.SerializeObject(propertyInfo));
}
string result = $"{{\"componentType\": \"{componentType}\", \"gameObjectPath\": \"{gameObjectPath}\", \"properties\": [{string.Join(",", properties)}]}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (System.Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to inspect SerializedObject: {e.Message}", id);
}
}
/// <summary>
/// Validates all components in a GameObject for missing references
/// </summary>
public static string ValidateMissingReferences(string paramsJson, object id)
{
try
{
string gameObjectPath = ExtractStringParam(paramsJson, "gameObjectPath");
if (string.IsNullOrEmpty(gameObjectPath))
{
return JsonRpcResponseHelper.ErrorMessage("gameObjectPath is required", id);
}
// Find GameObject
GameObject target = GameObject.Find(gameObjectPath);
if (target == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject not found: {gameObjectPath}", id);
}
var missingReferences = new List<string>();
Component[] components = target.GetComponents<Component>();
foreach (Component component in components)
{
if (component == null)
{
missingReferences.Add(JsonHelper.SerializeObject(new Dictionary<string, object>
{
{ "componentType", "Unknown" },
{ "issue", "Component is null (missing script)" }
}));
continue;
}
SerializedObject serializedObject = new SerializedObject(component);
SerializedProperty property = serializedObject.GetIterator();
while (property.NextVisible(true))
{
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
// Check for missing reference
if (property.objectReferenceValue == null && property.objectReferenceInstanceIDValue != 0)
{
missingReferences.Add(JsonHelper.SerializeObject(new Dictionary<string, object>
{
{ "componentType", component.GetType().Name },
{ "propertyPath", property.propertyPath },
{ "propertyName", property.name },
{ "displayName", property.displayName },
{ "instanceID", property.objectReferenceInstanceIDValue }
}));
}
}
}
}
string result = $"{{\"gameObjectPath\": \"{gameObjectPath}\", \"missingReferences\": [{string.Join(",", missingReferences)}], \"count\": {missingReferences.Count}}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (System.Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to validate missing references: {e.Message}", id);
}
}
/// <summary>
/// Validates all GameObjects in the current scene for missing references
/// </summary>
public static string ValidateSceneMissingReferences(string paramsJson, object id)
{
try
{
var allMissingReferences = new List<string>();
GameObject[] allGameObjects = Object.FindObjectsOfType<GameObject>();
foreach (GameObject go in allGameObjects)
{
Component[] components = go.GetComponents<Component>();
foreach (Component component in components)
{
if (component == null)
{
allMissingReferences.Add(JsonHelper.SerializeObject(new Dictionary<string, object>
{
{ "gameObjectPath", go.name },
{ "componentType", "Unknown" },
{ "issue", "Component is null (missing script)" }
}));
continue;
}
SerializedObject serializedObject = new SerializedObject(component);
SerializedProperty property = serializedObject.GetIterator();
while (property.NextVisible(true))
{
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
if (property.objectReferenceValue == null && property.objectReferenceInstanceIDValue != 0)
{
allMissingReferences.Add(JsonHelper.SerializeObject(new Dictionary<string, object>
{
{ "gameObjectPath", go.name },
{ "componentType", component.GetType().Name },
{ "propertyPath", property.propertyPath },
{ "propertyName", property.name },
{ "displayName", property.displayName },
{ "instanceID", property.objectReferenceInstanceIDValue }
}));
}
}
}
}
}
string result = $"{{\"totalGameObjects\": {allGameObjects.Length}, \"missingReferences\": [{string.Join(",", allMissingReferences)}], \"count\": {allMissingReferences.Count}}}";
return JsonRpcResponseHelper.SuccessMessage(result, id);
}
catch (System.Exception e)
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to validate scene missing references: {e.Message}", id);
}
}
private static string ExtractStringParam(string paramsJson, string paramName)
{
try
{
string pattern = $"\"{paramName}\"\\s*:\\s*\"([^\"]+)\"";
var match = System.Text.RegularExpressions.Regex.Match(paramsJson, pattern);
if (match.Success)
{
return match.Groups[1].Value;
}
}
catch (System.Exception e)
{
Debug.LogError($"Error extracting parameter '{paramName}': {e.Message}");
}
return null;
}
}
/// <summary>
/// Helper class for JSON serialization
/// </summary>
public static class JsonHelper
{
public static string SerializeObject(Dictionary<string, object> dict)
{
var pairs = new List<string>();
foreach (var kvp in dict)
{
string value;
if (kvp.Value is string)
{
value = $"\"{EscapeJson(kvp.Value.ToString())}\"";
}
else if (kvp.Value is bool)
{
value = kvp.Value.ToString().ToLower();
}
else
{
value = kvp.Value.ToString();
}
pairs.Add($"\"{kvp.Key}\": {value}");
}
return "{" + string.Join(", ", pairs) + "}";
}
private static string EscapeJson(string str)
{
return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
#endif
}