Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions MCPForUnity/Editor/Helpers/GameObjectSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,73 @@ private static bool IsOrDerivedFrom(Type type, string baseTypeFullName)
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<string> _crashingTypeNames = new HashSet<string>
{
"Fusion.NetworkBehaviourBuffer",
"Fusion.NetworkBehaviourCallbackBuffer",
"Fusion.Networked+Internals",
"Fusion.Changed`1",
};
private static readonly PropertyInfo _isByRefLikeProperty = typeof(Type).GetProperty("IsByRefLike");

/// <summary>
/// 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.
/// </summary>
private static bool IsUnsafeType(Type type)
{
return IsUnsafeType(type, new HashSet<Type>());
}

private static bool IsUnsafeType(Type type, HashSet<Type> 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;
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -352,6 +419,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ
{
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
// Skip properties whose return type would crash when accessed via reflection
// (e.g. Fusion IL-weaved types, Span<>, ReadOnlySpan<>, pointers)
if (IsUnsafeType(propInfo.PropertyType)) continue;
// Add if not already added (handles overrides - keep the most derived version)
if (!propertiesToCache.Any(p => p.Name == propInfo.Name))
{
Expand All @@ -367,6 +437,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ
foreach (var fieldInfo in declaredFields)
{
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
// Skip fields whose type would crash when accessed via reflection
// (e.g. Fusion IL-weaved types, Span<>, ReadOnlySpan<>, pointers)
if (IsUnsafeType(fieldInfo.FieldType)) continue;

// Add if not already added (handles hiding - keep the most derived version)
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using MCPForUnity.Editor.Helpers;
using NUnit.Framework;
using UnityEngine;

namespace Fusion
{
public struct NetworkBehaviourBuffer
{
public int Value;
}

public struct Changed<T>
{
public T Value;
}
}

namespace MCPForUnityTests.Editor.Tools
{
public class FusionUnsafeTypeSerializationTests
{
[Test]
public void GetComponentData_SkipsFusionUnsafeTypesInsideContainers()
{
var testObject = new GameObject("FusionUnsafeTypeTestObject");

try
{
var component = testObject.AddComponent<FusionUnsafeTypeComponent>();
component.bufferList.Add(new Fusion.NetworkBehaviourBuffer { Value = 1 });
component.nestedChangedLookup["changed"] = new List<Fusion.Changed<int>>
{
new Fusion.Changed<int> { Value = 2 }
};
component.ChangedListProperty.Add(new Fusion.Changed<int> { Value = 3 });

var result = GameObjectSerializer.GetComponentData(component) as Dictionary<string, object>;

Assert.IsNotNull(result, "GetComponentData should return dictionary data.");
Assert.IsTrue(result.TryGetValue("properties", out object propertiesObject), "Serialized data should contain properties.");

var properties = propertiesObject as Dictionary<string, object>;
Assert.IsNotNull(properties, "Serialized properties should be a dictionary.");

Assert.IsTrue(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.safeValue)), "Safe fields should still serialize.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.directBuffer)), "Direct Fusion buffer fields should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.bufferList)), "Collections containing Fusion buffer types should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.nestedChangedLookup)), "Nested generic containers containing Fusion Changed<T> should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.ChangedListProperty)), "Properties returning collections of Fusion Changed<T> should be skipped.");
}
finally
{
UnityEngine.Object.DestroyImmediate(testObject);
}
}
}

public sealed class FusionUnsafeTypeComponent : MonoBehaviour
{
public string safeValue = "kept";
public Fusion.NetworkBehaviourBuffer directBuffer;
public List<Fusion.NetworkBehaviourBuffer> bufferList = new List<Fusion.NetworkBehaviourBuffer>();
public Dictionary<string, List<Fusion.Changed<int>>> nestedChangedLookup = new Dictionary<string, List<Fusion.Changed<int>>>();

public List<Fusion.Changed<int>> ChangedListProperty { get; } = new List<Fusion.Changed<int>>();
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.