@@ -119,6 +119,39 @@ public static JObject GameObjectToJObject(GameObject gameObject, bool includeDet
119119 return gameObjectJson ;
120120 }
121121
122+ /// <summary>
123+ /// Namespace prefixes for components with native/C++ code that crash when accessed via reflection.
124+ /// These components will only have basic info (type, enabled) serialized, not detailed properties.
125+ /// </summary>
126+ private static readonly string [ ] UnsafeNamespacePrefixes = new string [ ]
127+ {
128+ "Pathfinding" , // A* Pathfinding Project
129+ "FMOD" , // FMOD audio
130+ "FMODUnity" , // FMOD Unity integration
131+ } ;
132+
133+ /// <summary>
134+ /// Check if a component type is from a native plugin that may crash when accessed via reflection
135+ /// </summary>
136+ private static bool IsUnsafeNativeComponent ( Type componentType )
137+ {
138+ if ( componentType == null ) return true ;
139+
140+ string fullName = componentType . FullName ?? "" ;
141+ string namespaceName = componentType . Namespace ?? "" ;
142+
143+ foreach ( string prefix in UnsafeNamespacePrefixes )
144+ {
145+ if ( namespaceName . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) ||
146+ fullName . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) )
147+ {
148+ return true ;
149+ }
150+ }
151+
152+ return false ;
153+ }
154+
122155 /// <summary>
123156 /// Get information about the components attached to a GameObject
124157 /// </summary>
@@ -134,16 +167,29 @@ private static JArray GetComponentsInfo(GameObject gameObject, bool includeDetai
134167 {
135168 if ( component == null ) continue ;
136169
170+ Type componentType = component . GetType ( ) ;
171+ bool isUnsafe = IsUnsafeNativeComponent ( componentType ) ;
172+
137173 JObject componentJson = new JObject
138174 {
139- [ "type" ] = component . GetType ( ) . Name ,
175+ [ "type" ] = componentType . Name ,
140176 [ "enabled" ] = IsComponentEnabled ( component )
141177 } ;
142178
143- // Add detailed information if requested
179+ // Add detailed information if requested and component is safe to inspect
144180 if ( includeDetailedInfo )
145181 {
146- componentJson [ "properties" ] = GetComponentProperties ( component ) ;
182+ if ( isUnsafe )
183+ {
184+ componentJson [ "properties" ] = new JObject
185+ {
186+ [ "_skipped" ] = "Native plugin component - serialization skipped for safety"
187+ } ;
188+ }
189+ else
190+ {
191+ componentJson [ "properties" ] = GetComponentProperties ( component ) ;
192+ }
147193 }
148194
149195 componentsArray . Add ( componentJson ) ;
@@ -181,6 +227,16 @@ private static bool IsComponentEnabled(Component component)
181227 return true ;
182228 }
183229
230+ /// <summary>
231+ /// Maximum depth for serializing nested objects to prevent stack overflow from circular references
232+ /// </summary>
233+ private const int MaxSerializationDepth = 5 ;
234+
235+ /// <summary>
236+ /// Maximum items to serialize in a collection to prevent excessive output
237+ /// </summary>
238+ private const int MaxCollectionItems = 50 ;
239+
184240 /// <summary>
185241 /// Get all serialized fields, public fields and public properties of a component
186242 /// </summary>
@@ -192,6 +248,9 @@ private static JObject GetComponentProperties(Component component)
192248
193249 JObject propertiesJson = new JObject ( ) ;
194250 Type componentType = component . GetType ( ) ;
251+
252+ // Track visited objects to prevent circular reference loops
253+ HashSet < object > visited = new HashSet < object > ( new ReferenceEqualityComparer ( ) ) ;
195254
196255 // Get serialized fields (both public and private with SerializeField attribute)
197256 FieldInfo [ ] fields = componentType . GetFields ( BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Instance ) ;
@@ -204,7 +263,7 @@ private static JObject GetComponentProperties(Component component)
204263 try
205264 {
206265 object value = field . GetValue ( component ) ;
207- propertiesJson [ field . Name ] = SerializeValue ( value ) ;
266+ propertiesJson [ field . Name ] = SerializeValue ( value , 0 , visited ) ;
208267 }
209268 catch ( Exception )
210269 {
@@ -223,7 +282,7 @@ private static JObject GetComponentProperties(Component component)
223282 try
224283 {
225284 object value = property . GetValue ( component ) ;
226- propertiesJson [ property . Name ] = SerializeValue ( value ) ;
285+ propertiesJson [ property . Name ] = SerializeValue ( value , 0 , visited ) ;
227286 }
228287 catch ( Exception )
229288 {
@@ -234,6 +293,15 @@ private static JObject GetComponentProperties(Component component)
234293
235294 return propertiesJson ;
236295 }
296+
297+ /// <summary>
298+ /// Reference equality comparer for tracking visited objects (prevents circular reference infinite loops)
299+ /// </summary>
300+ private class ReferenceEqualityComparer : IEqualityComparer < object >
301+ {
302+ public new bool Equals ( object x , object y ) => ReferenceEquals ( x , y ) ;
303+ public int GetHashCode ( object obj ) => System . Runtime . CompilerServices . RuntimeHelpers . GetHashCode ( obj ) ;
304+ }
237305
238306 /// <summary>
239307 /// Determine if a property should be skipped during serialization
@@ -254,14 +322,34 @@ private static bool ShouldSkipProperty(PropertyInfo property)
254322 }
255323
256324 /// <summary>
257- /// Serialize a value to a JToken
325+ /// Serialize a value to a JToken with depth limiting and circular reference protection
258326 /// </summary>
259327 /// <param name="value">The value to serialize</param>
328+ /// <param name="depth">Current recursion depth</param>
329+ /// <param name="visited">Set of already visited reference objects to detect circular references</param>
260330 /// <returns>A JToken representing the value</returns>
261- private static JToken SerializeValue ( object value )
331+ private static JToken SerializeValue ( object value , int depth = 0 , HashSet < object > visited = null )
262332 {
263333 if ( value == null )
264334 return JValue . CreateNull ( ) ;
335+
336+ // Depth limit check to prevent stack overflow
337+ if ( depth > MaxSerializationDepth )
338+ return "[max depth exceeded]" ;
339+
340+ Type valueType = value . GetType ( ) ;
341+
342+ // For reference types (excluding strings), check for circular references
343+ if ( ! valueType . IsValueType && ! ( value is string ) )
344+ {
345+ if ( visited == null )
346+ visited = new HashSet < object > ( new ReferenceEqualityComparer ( ) ) ;
347+
348+ if ( visited . Contains ( value ) )
349+ return "[circular reference]" ;
350+
351+ visited . Add ( value ) ;
352+ }
265353
266354 // Handle common Unity types
267355 if ( value is Vector2 vector2 )
@@ -281,8 +369,8 @@ private static JToken SerializeValue(object value)
281369
282370 if ( value is Bounds bounds )
283371 return new JObject {
284- [ "center" ] = SerializeValue ( bounds . center ) ,
285- [ "size" ] = SerializeValue ( bounds . size )
372+ [ "center" ] = SerializeValue ( bounds . center , depth + 1 , visited ) ,
373+ [ "size" ] = SerializeValue ( bounds . size , depth + 1 , visited )
286374 } ;
287375
288376 if ( value is Rect rect )
@@ -291,24 +379,38 @@ private static JToken SerializeValue(object value)
291379 if ( value is UnityEngine . Object unityObject )
292380 return unityObject != null ? unityObject . name : null ;
293381
294- // Handle arrays and lists
382+ // Handle arrays and lists with item limit
295383 if ( value is System . Collections . IList list )
296384 {
297385 JArray array = new JArray ( ) ;
386+ int count = 0 ;
298387 foreach ( var item in list )
299388 {
300- array . Add ( SerializeValue ( item ) ) ;
389+ if ( count >= MaxCollectionItems )
390+ {
391+ array . Add ( $ "[... and { list . Count - count } more items]") ;
392+ break ;
393+ }
394+ array . Add ( SerializeValue ( item , depth + 1 , visited ) ) ;
395+ count ++ ;
301396 }
302397 return array ;
303398 }
304399
305- // Handle dictionaries
400+ // Handle dictionaries with item limit
306401 if ( value is System . Collections . IDictionary dict )
307402 {
308403 JObject obj = new JObject ( ) ;
404+ int count = 0 ;
309405 foreach ( System . Collections . DictionaryEntry entry in dict )
310406 {
311- obj [ entry . Key . ToString ( ) ] = SerializeValue ( entry . Value ) ;
407+ if ( count >= MaxCollectionItems )
408+ {
409+ obj [ "_truncated" ] = $ "{ dict . Count - count } more entries";
410+ break ;
411+ }
412+ obj [ entry . Key . ToString ( ) ] = SerializeValue ( entry . Value , depth + 1 , visited ) ;
413+ count ++ ;
312414 }
313415 return obj ;
314416 }
@@ -317,8 +419,14 @@ private static JToken SerializeValue(object value)
317419 if ( value is Enum enumValue )
318420 return enumValue . ToString ( ) ;
319421
320- // Use default serialization for primitive types
321- return JToken . FromObject ( value ) ;
422+ // Handle primitive types directly
423+ if ( valueType . IsPrimitive || value is string || value is decimal )
424+ {
425+ return JToken . FromObject ( value ) ;
426+ }
427+
428+ // For complex types we don't recognize, return type name to avoid unsafe deep serialization
429+ return $ "[{ valueType . Name } ]";
322430 }
323431 }
324432}
0 commit comments