1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Reflection ;
5+ using Newtonsoft . Json ;
6+ using Newtonsoft . Json . Linq ;
7+ using UnityEditor ;
8+ using UnityEngine ;
9+ using UnityMcpBridge . Runtime . Serialization ; // For Converters
10+
11+ namespace UnityMcpBridge . Editor . Helpers
12+ {
13+ /// <summary>
14+ /// Handles serialization of GameObjects and Components for MCP responses.
15+ /// Includes reflection helpers and caching for performance.
16+ /// </summary>
17+ public static class GameObjectSerializer
18+ {
19+ // --- Data Serialization ---
20+
21+ /// <summary>
22+ /// Creates a serializable representation of a GameObject.
23+ /// </summary>
24+ public static object GetGameObjectData ( GameObject go )
25+ {
26+ if ( go == null )
27+ return null ;
28+ return new
29+ {
30+ name = go . name ,
31+ instanceID = go . GetInstanceID ( ) ,
32+ tag = go . tag ,
33+ layer = go . layer ,
34+ activeSelf = go . activeSelf ,
35+ activeInHierarchy = go . activeInHierarchy ,
36+ isStatic = go . isStatic ,
37+ scenePath = go . scene . path , // Identify which scene it belongs to
38+ transform = new // Serialize transform components carefully to avoid JSON issues
39+ {
40+ // Serialize Vector3 components individually to prevent self-referencing loops.
41+ // The default serializer can struggle with properties like Vector3.normalized.
42+ position = new
43+ {
44+ x = go . transform . position . x ,
45+ y = go . transform . position . y ,
46+ z = go . transform . position . z ,
47+ } ,
48+ localPosition = new
49+ {
50+ x = go . transform . localPosition . x ,
51+ y = go . transform . localPosition . y ,
52+ z = go . transform . localPosition . z ,
53+ } ,
54+ rotation = new
55+ {
56+ x = go . transform . rotation . eulerAngles . x ,
57+ y = go . transform . rotation . eulerAngles . y ,
58+ z = go . transform . rotation . eulerAngles . z ,
59+ } ,
60+ localRotation = new
61+ {
62+ x = go . transform . localRotation . eulerAngles . x ,
63+ y = go . transform . localRotation . eulerAngles . y ,
64+ z = go . transform . localRotation . eulerAngles . z ,
65+ } ,
66+ scale = new
67+ {
68+ x = go . transform . localScale . x ,
69+ y = go . transform . localScale . y ,
70+ z = go . transform . localScale . z ,
71+ } ,
72+ forward = new
73+ {
74+ x = go . transform . forward . x ,
75+ y = go . transform . forward . y ,
76+ z = go . transform . forward . z ,
77+ } ,
78+ up = new
79+ {
80+ x = go . transform . up . x ,
81+ y = go . transform . up . y ,
82+ z = go . transform . up . z ,
83+ } ,
84+ right = new
85+ {
86+ x = go . transform . right . x ,
87+ y = go . transform . right . y ,
88+ z = go . transform . right . z ,
89+ } ,
90+ } ,
91+ parentInstanceID = go . transform . parent ? . gameObject . GetInstanceID ( ) ?? 0 , // 0 if no parent
92+ // Optionally include components, but can be large
93+ // components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
94+ // Or just component names:
95+ componentNames = go . GetComponents < Component > ( )
96+ . Select ( c => c . GetType ( ) . FullName )
97+ . ToList ( ) ,
98+ } ;
99+ }
100+
101+ // --- Metadata Caching for Reflection ---
102+ private class CachedMetadata
103+ {
104+ public readonly List < PropertyInfo > SerializableProperties ;
105+ public readonly List < FieldInfo > SerializableFields ;
106+
107+ public CachedMetadata ( List < PropertyInfo > properties , List < FieldInfo > fields )
108+ {
109+ SerializableProperties = properties ;
110+ SerializableFields = fields ;
111+ }
112+ }
113+ // Key becomes Tuple<Type, bool>
114+ private static readonly Dictionary < Tuple < Type , bool > , CachedMetadata > _metadataCache = new Dictionary < Tuple < Type , bool > , CachedMetadata > ( ) ;
115+ // --- End Metadata Caching ---
116+
117+ /// <summary>
118+ /// Creates a serializable representation of a Component, attempting to serialize
119+ /// public properties and fields using reflection, with caching and control over non-public fields.
120+ /// </summary>
121+ // Add the flag parameter here
122+ public static object GetComponentData ( Component c , bool includeNonPublicSerializedFields = true )
123+ {
124+ if ( c == null ) return null ;
125+ Type componentType = c . GetType ( ) ;
126+
127+ var data = new Dictionary < string , object >
128+ {
129+ { "typeName" , componentType . FullName } ,
130+ { "instanceID" , c . GetInstanceID ( ) }
131+ } ;
132+
133+ // --- Get Cached or Generate Metadata (using new cache key) ---
134+ Tuple < Type , bool > cacheKey = new Tuple < Type , bool > ( componentType , includeNonPublicSerializedFields ) ;
135+ if ( ! _metadataCache . TryGetValue ( cacheKey , out CachedMetadata cachedData ) )
136+ {
137+ var propertiesToCache = new List < PropertyInfo > ( ) ;
138+ var fieldsToCache = new List < FieldInfo > ( ) ;
139+
140+ // Traverse the hierarchy from the component type up to MonoBehaviour
141+ Type currentType = componentType ;
142+ while ( currentType != null && currentType != typeof ( MonoBehaviour ) && currentType != typeof ( object ) )
143+ {
144+ // Get properties declared only at the current type level
145+ BindingFlags propFlags = BindingFlags . Public | BindingFlags . Instance | BindingFlags . DeclaredOnly ;
146+ foreach ( var propInfo in currentType . GetProperties ( propFlags ) )
147+ {
148+ // Basic filtering (readable, not indexer, not transform which is handled elsewhere)
149+ if ( ! propInfo . CanRead || propInfo . GetIndexParameters ( ) . Length > 0 || propInfo . Name == "transform" ) continue ;
150+ // Add if not already added (handles overrides - keep the most derived version)
151+ if ( ! propertiesToCache . Any ( p => p . Name == propInfo . Name ) ) {
152+ propertiesToCache . Add ( propInfo ) ;
153+ }
154+ }
155+
156+ // Get fields declared only at the current type level (both public and non-public)
157+ BindingFlags fieldFlags = BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . DeclaredOnly ;
158+ var declaredFields = currentType . GetFields ( fieldFlags ) ;
159+
160+ // Process the declared Fields for caching
161+ foreach ( var fieldInfo in declaredFields )
162+ {
163+ if ( fieldInfo . Name . EndsWith ( "k__BackingField" ) ) continue ; // Skip backing fields
164+
165+ // Add if not already added (handles hiding - keep the most derived version)
166+ if ( fieldsToCache . Any ( f => f . Name == fieldInfo . Name ) ) continue ;
167+
168+ bool shouldInclude = false ;
169+ if ( includeNonPublicSerializedFields )
170+ {
171+ // If TRUE, include Public OR NonPublic with [SerializeField]
172+ shouldInclude = fieldInfo . IsPublic || ( fieldInfo . IsPrivate && fieldInfo . IsDefined ( typeof ( SerializeField ) , inherit : false ) ) ;
173+ }
174+ else // includeNonPublicSerializedFields is FALSE
175+ {
176+ // If FALSE, include ONLY if it is explicitly Public.
177+ shouldInclude = fieldInfo . IsPublic ;
178+ }
179+
180+ if ( shouldInclude )
181+ {
182+ fieldsToCache . Add ( fieldInfo ) ;
183+ }
184+ }
185+
186+ // Move to the base type
187+ currentType = currentType . BaseType ;
188+ }
189+ // --- End Hierarchy Traversal ---
190+
191+ cachedData = new CachedMetadata ( propertiesToCache , fieldsToCache ) ;
192+ _metadataCache [ cacheKey ] = cachedData ; // Add to cache with combined key
193+ }
194+ // --- End Get Cached or Generate Metadata ---
195+
196+ // --- Use cached metadata ---
197+ var serializablePropertiesOutput = new Dictionary < string , object > ( ) ;
198+ // Use cached properties
199+ foreach ( var propInfo in cachedData . SerializableProperties )
200+ {
201+ // --- Skip known obsolete/problematic Component shortcut properties ---
202+ string propName = propInfo . Name ;
203+ if ( propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
204+ propName == "light" || propName == "animation" || propName == "constantForce" ||
205+ propName == "renderer" || propName == "audio" || propName == "networkView" ||
206+ propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
207+ propName == "particleSystem" ||
208+ // Also skip potentially problematic Matrix properties prone to cycles/errors
209+ propName == "worldToLocalMatrix" || propName == "localToWorldMatrix" )
210+ {
211+ continue ; // Skip these properties
212+ }
213+ // --- End Skip ---
214+
215+ try
216+ {
217+ object value = propInfo . GetValue ( c ) ;
218+ Type propType = propInfo . PropertyType ;
219+ AddSerializableValue ( serializablePropertiesOutput , propName , propType , value ) ;
220+ }
221+ catch ( Exception ex )
222+ {
223+ Debug . LogWarning ( $ "Could not read property { propName } on { componentType . Name } : { ex . Message } ") ;
224+ }
225+ }
226+
227+ // Use cached fields
228+ foreach ( var fieldInfo in cachedData . SerializableFields )
229+ {
230+ try
231+ {
232+ object value = fieldInfo . GetValue ( c ) ;
233+ string fieldName = fieldInfo . Name ;
234+ Type fieldType = fieldInfo . FieldType ;
235+ AddSerializableValue ( serializablePropertiesOutput , fieldName , fieldType , value ) ;
236+ }
237+ catch ( Exception ex )
238+ {
239+ Debug . LogWarning ( $ "Could not read field { fieldInfo . Name } on { componentType . Name } : { ex . Message } ") ;
240+ }
241+ }
242+ // --- End Use cached metadata ---
243+
244+ if ( serializablePropertiesOutput . Count > 0 )
245+ {
246+ data [ "properties" ] = serializablePropertiesOutput ;
247+ }
248+
249+ return data ;
250+ }
251+
252+ // Helper function to decide how to serialize different types
253+ private static void AddSerializableValue ( Dictionary < string , object > dict , string name , Type type , object value )
254+ {
255+ // Simplified: Directly use CreateTokenFromValue which uses the serializer
256+ if ( value == null )
257+ {
258+ dict [ name ] = null ;
259+ return ;
260+ }
261+
262+ try
263+ {
264+ // Use the helper that employs our custom serializer settings
265+ JToken token = CreateTokenFromValue ( value , type ) ;
266+ if ( token != null ) // Check if serialization succeeded in the helper
267+ {
268+ // Convert JToken back to a basic object structure for the dictionary
269+ dict [ name ] = ConvertJTokenToPlainObject ( token ) ;
270+ }
271+ // If token is null, it means serialization failed and a warning was logged.
272+ }
273+ catch ( Exception e )
274+ {
275+ // Catch potential errors during JToken conversion or addition to dictionary
276+ Debug . LogWarning ( $ "[AddSerializableValue] Error processing value for '{ name } ' (Type: { type . FullName } ): { e . Message } . Skipping.") ;
277+ }
278+ }
279+
280+ // Helper to convert JToken back to basic object structure
281+ private static object ConvertJTokenToPlainObject ( JToken token )
282+ {
283+ if ( token == null ) return null ;
284+
285+ switch ( token . Type )
286+ {
287+ case JTokenType . Object :
288+ var objDict = new Dictionary < string , object > ( ) ;
289+ foreach ( var prop in ( ( JObject ) token ) . Properties ( ) )
290+ {
291+ objDict [ prop . Name ] = ConvertJTokenToPlainObject ( prop . Value ) ;
292+ }
293+ return objDict ;
294+
295+ case JTokenType . Array :
296+ var list = new List < object > ( ) ;
297+ foreach ( var item in ( JArray ) token )
298+ {
299+ list . Add ( ConvertJTokenToPlainObject ( item ) ) ;
300+ }
301+ return list ;
302+
303+ case JTokenType . Integer :
304+ return token . ToObject < long > ( ) ; // Use long for safety
305+ case JTokenType . Float :
306+ return token . ToObject < double > ( ) ; // Use double for safety
307+ case JTokenType . String :
308+ return token . ToObject < string > ( ) ;
309+ case JTokenType . Boolean :
310+ return token . ToObject < bool > ( ) ;
311+ case JTokenType . Date :
312+ return token . ToObject < DateTime > ( ) ;
313+ case JTokenType . Guid :
314+ return token . ToObject < Guid > ( ) ;
315+ case JTokenType . Uri :
316+ return token . ToObject < Uri > ( ) ;
317+ case JTokenType . TimeSpan :
318+ return token . ToObject < TimeSpan > ( ) ;
319+ case JTokenType . Bytes :
320+ return token . ToObject < byte [ ] > ( ) ;
321+ case JTokenType . Null :
322+ return null ;
323+ case JTokenType . Undefined :
324+ return null ; // Treat undefined as null
325+
326+ default :
327+ // Fallback for simple value types not explicitly listed
328+ if ( token is JValue jValue && jValue . Value != null )
329+ {
330+ return jValue . Value ;
331+ }
332+ Debug . LogWarning ( $ "Unsupported JTokenType encountered: { token . Type } . Returning null.") ;
333+ return null ;
334+ }
335+ }
336+
337+ // --- Define custom JsonSerializerSettings for OUTPUT ---
338+ private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
339+ {
340+ Converters = new List < JsonConverter >
341+ {
342+ new Vector3Converter ( ) ,
343+ new Vector2Converter ( ) ,
344+ new QuaternionConverter ( ) ,
345+ new ColorConverter ( ) ,
346+ new RectConverter ( ) ,
347+ new BoundsConverter ( ) ,
348+ new UnityEngineObjectConverter ( ) // Handles serialization of references
349+ } ,
350+ ReferenceLoopHandling = ReferenceLoopHandling . Ignore ,
351+ // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
352+ } ;
353+ private static readonly JsonSerializer _outputSerializer = JsonSerializer . Create ( _outputSerializerSettings ) ;
354+ // --- End Define custom JsonSerializerSettings ---
355+
356+ // Helper to create JToken using the output serializer
357+ private static JToken CreateTokenFromValue ( object value , Type type )
358+ {
359+ if ( value == null ) return JValue . CreateNull ( ) ;
360+
361+ try
362+ {
363+ // Use the pre-configured OUTPUT serializer instance
364+ return JToken . FromObject ( value , _outputSerializer ) ;
365+ }
366+ catch ( JsonSerializationException e )
367+ {
368+ Debug . LogWarning ( $ "[GameObjectSerializer] Newtonsoft.Json Error serializing value of type { type . FullName } : { e . Message } . Skipping property/field.") ;
369+ return null ; // Indicate serialization failure
370+ }
371+ catch ( Exception e ) // Catch other unexpected errors
372+ {
373+ Debug . LogWarning ( $ "[GameObjectSerializer] Unexpected error serializing value of type { type . FullName } : { e } . Skipping property/field.") ;
374+ return null ; // Indicate serialization failure
375+ }
376+ }
377+ }
378+ }
0 commit comments