/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
#if UNITY_6000_5_OR_NEWER
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using com.IvanMurzak.ReflectorNet;
using com.IvanMurzak.ReflectorNet.Model;
using com.IvanMurzak.ReflectorNet.Utils;
using AIGD;
using com.IvanMurzak.Unity.MCP.Runtime.Extensions;
using Microsoft.Extensions.Logging;
using UnityEngine;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace com.IvanMurzak.Unity.MCP.Reflection.Converter
{
///
/// Reflection converter for UnityEngine.GameObject
/// IMPORTANT: it implements custom depth handling to avoid heavy serialization of Unity objects.
/// As a result it only serializes a GameObject at depth == 0
///
public partial class UnityEngine_GameObject_ReflectionConverter : UnityGenericReflectionConverter
{
// Allow SetValue so an atomic GameObject-ref node (e.g. {"instanceID": ...}) on the
// merge-patch (Reflector.TryPatch) path is resolved to the live GameObject and assigned
// to the target field. AllowSetValue also gates the non-merge-patch TryModify path, but
// the overridden SetValue below is a reference-only resolve
// (ToGameObjectRef().FindGameObject()) — never a structural write or GameObject
// instantiation — so flipping it cannot cause unintended writes elsewhere, and it does
// not reintroduce the heavy GameObject serialization this converter deliberately avoids.
// See IvanMurzak/Unity-MCP#791.
public override bool AllowSetValue => true;
// This converter does NOT inherit UnityEngine_Object_ReflectionConverter (it extends
// UnityGenericReflectionConverter directly), so it needs its own override.
// Treat GameObject-ref JSON nodes (e.g. {"instanceID": ...}) as atomic on the merge-patch
// (Reflector.TryPatch) path so they resolve to the live GameObject instead of being
// structurally descended. Safe because ReflectorNet 5.3.0 only applies atomic delegation
// to non-root nodes. See IvanMurzak/Unity-MCP#791.
public override bool TreatJsonObjectAsAtomicValue(Type type) => true;
protected override IEnumerable GetIgnoredProperties()
{
foreach (var property in base.GetIgnoredProperties())
yield return property;
yield return nameof(UnityEngine.GameObject.gameObject);
yield return nameof(UnityEngine.GameObject.transform);
yield return nameof(UnityEngine.GameObject.scene);
#if UNITY_6000_3_OR_NEWER
yield return nameof(UnityEngine.GameObject.transformHandle);
#endif
}
protected override SerializedMember InternalSerialize(
Reflector reflector,
object? obj,
Type type,
string? name = null,
bool recursive = true,
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
int depth = 0, Logs? logs = null,
ILogger? logger = null,
SerializationContext? context = null)
{
if (obj == null)
return SerializedMember.Null(type, name);
var unityObject = obj as UnityEngine.GameObject;
if (unityObject == null)
{
// UnityEngine.Object is destroyed but reference is not null
return SerializedMember.Null(type, name);
}
var objectRef = new GameObjectRef(unityObject.GetEntityId());
if (depth >= 1 || !recursive)
{
return SerializedMember.FromValue(
reflector: reflector,
type: type,
value: objectRef,
name: name ?? unityObject.name);
}
return new SerializedMember()
{
name = name ?? unityObject.name,
typeName = type.GetTypeId(),
fields = SerializeFields(
reflector,
obj: obj,
flags: flags,
depth: depth + 1,
logs: logs,
logger: logger,
context: context),
props = SerializeProperties(
reflector,
obj: obj,
flags: flags,
depth: depth + 1,
logs: logs,
logger: logger,
context: context)
}.SetValue(reflector, objectRef);
}
protected override bool SetValue(
Reflector reflector,
ref object? obj,
Type type,
JsonElement? value,
int depth = 0,
Logs? logs = null,
ILogger? logger = null)
{
var padding = StringUtils.GetPadding(depth);
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}Set value type='{type.GetTypeId()}'. Converter='{GetType().GetTypeShortName()}'.");
try
{
obj = value
.ToGameObjectRef(
reflector: reflector,
suppressException: false,
depth: depth,
logs: logs,
logger: logger)
.FindGameObject();
return true;
}
catch (Exception ex)
{
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError(ex, $"{padding}[Error] Failed to deserialize value for type '{type.GetTypeId()}'. Converter: {GetType().GetTypeShortName()}. Exception: {ex.Message}");
logs?.Error($"Failed to set value for type '{type.GetTypeId()}'. Converter: {GetType().GetTypeShortName()}. Exception: {ex.Message}", depth);
return false;
}
}
public override object? Deserialize(
Reflector reflector,
SerializedMember data,
Type? fallbackType = null,
string? fallbackName = null,
int depth = 0,
Logs? logs = null,
ILogger? logger = null,
DeserializationContext? context = null)
{
return data.valueJsonElement
.ToGameObjectRef(
reflector: reflector,
depth: depth,
logs: logs,
logger: logger)
.FindGameObject();
}
protected override object? DeserializeValueAsJsonElement(
Reflector reflector,
SerializedMember data,
Type type,
int depth = 0,
Logs? logs = null,
ILogger? logger = null)
{
return data.valueJsonElement
.ToGameObjectRef(
reflector: reflector,
depth: depth,
logs: logs,
logger: logger)
.FindGameObject();
}
protected override bool TryModifyProperty(
Reflector reflector,
ref object obj,
Type objType,
SerializedMember member,
int depth = 0,
Logs? logs = null,
BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
ILogger? logger = null)
{
try
{
if (obj is UnityEngine.GameObject go)
{
#if !UNITY_6000_5_OR_NEWER
#pragma warning disable CS0618 // Type or member is obsolete
if (member.name == nameof(GameObject.active))
{
go.SetActive(member.GetValue(reflector));
return true;
}
#pragma warning restore CS0618 // Type or member is obsolete
#endif
if (member.name == nameof(GameObject.activeSelf))
{
go.SetActive(member.GetValue(reflector));
return true;
}
}
}
catch (Exception e)
{
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError(e, "{padding}Failed to set property '{property}'",
StringUtils.GetPadding(depth), member.name);
logs?.Error($"Failed to set property '{member.name}': {e.Message}", depth);
return false;
}
return base.TryModifyProperty(
reflector,
ref obj,
objType,
member,
depth,
logs,
flags,
logger);
}
public override object? CreateInstance(Reflector reflector, Type type)
{
return new GameObject("New GameObject");
}
}
}
#endif