Skip to content

Commit 1d8bda3

Browse files
committed
fix: skip unsafe Fusion serialization types
1 parent a2a5edf commit 1d8bda3

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

MCPForUnity/Editor/Helpers/GameObjectSerializer.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,73 @@ private static bool IsOrDerivedFrom(Type type, string baseTypeFullName)
131131
return false;
132132
}
133133

134+
// Type full names that are known to crash the Editor when accessed via reflection.
135+
// Photon Fusion uses IL weaving to inject fields with these types into NetworkBehaviour
136+
// subclasses. They contain native/unmanaged memory and cannot be safely serialized.
137+
private static readonly HashSet<string> _crashingTypeNames = new HashSet<string>
138+
{
139+
"Fusion.NetworkBehaviourBuffer",
140+
"Fusion.NetworkBehaviourCallbackBuffer",
141+
"Fusion.Networked+Internals",
142+
"Fusion.Changed`1",
143+
};
144+
private static readonly PropertyInfo _isByRefLikeProperty = typeof(Type).GetProperty("IsByRefLike");
145+
146+
/// <summary>
147+
/// Checks if a type is unsafe to access via reflection or serialize.
148+
/// Returns true for ref structs (Span, ReadOnlySpan), pointer types,
149+
/// by-ref types, and known IL-weaved types that crash the Editor.
150+
/// </summary>
151+
private static bool IsUnsafeType(Type type)
152+
{
153+
return IsUnsafeType(type, new HashSet<Type>());
154+
}
155+
156+
private static bool IsUnsafeType(Type type, HashSet<Type> visitedTypes)
157+
{
158+
if (type == null) return false;
159+
if (!visitedTypes.Add(type)) return false;
160+
161+
// Pointer and by-ref types cannot be serialized
162+
if (type.IsPointer || type.IsByRef)
163+
return true;
164+
165+
// Ref structs (Span<>, ReadOnlySpan<>, etc.) cannot be boxed. Use reflection
166+
// so Unity versions without Type.IsByRefLike still compile.
167+
if (type.IsValueType && _isByRefLikeProperty != null && (bool)_isByRefLikeProperty.GetValue(type, null))
168+
return true;
169+
170+
// Check the type and its generic definition against the blacklist
171+
string fullName = type.FullName;
172+
if (fullName != null && _crashingTypeNames.Contains(fullName))
173+
return true;
174+
175+
if (type.IsGenericType)
176+
{
177+
string genericFullName = type.GetGenericTypeDefinition()?.FullName;
178+
if (genericFullName != null && _crashingTypeNames.Contains(genericFullName))
179+
return true;
180+
}
181+
182+
// Catch-all for Fusion buffer types injected by IL weaving
183+
if (fullName != null && fullName.StartsWith("Fusion.") && fullName.Contains("Buffer"))
184+
return true;
185+
186+
// Arrays and generic containers can wrap unsafe Fusion/ref-like types.
187+
// Newtonsoft.Json would still recurse into those values during serialization.
188+
Type elementType = type.GetElementType();
189+
if (elementType != null && IsUnsafeType(elementType, visitedTypes))
190+
return true;
191+
192+
foreach (Type genericArgument in type.GetGenericArguments())
193+
{
194+
if (IsUnsafeType(genericArgument, visitedTypes))
195+
return true;
196+
}
197+
198+
return false;
199+
}
200+
134201
/// <summary>
135202
/// Serializes a UnityEngine.Object reference to a dictionary with name, instanceID, and assetPath.
136203
/// Used for consistent serialization of asset references in special-case component handlers.
@@ -352,6 +419,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ
352419
{
353420
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
354421
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
422+
// Skip properties whose return type would crash when accessed via reflection
423+
// (e.g. Fusion IL-weaved types, Span<>, ReadOnlySpan<>, pointers)
424+
if (IsUnsafeType(propInfo.PropertyType)) continue;
355425
// Add if not already added (handles overrides - keep the most derived version)
356426
if (!propertiesToCache.Any(p => p.Name == propInfo.Name))
357427
{
@@ -367,6 +437,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ
367437
foreach (var fieldInfo in declaredFields)
368438
{
369439
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
440+
// Skip fields whose type would crash when accessed via reflection
441+
// (e.g. Fusion IL-weaved types, Span<>, ReadOnlySpan<>, pointers)
442+
if (IsUnsafeType(fieldInfo.FieldType)) continue;
370443

371444
// Add if not already added (handles hiding - keep the most derived version)
372445
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Generic;
2+
using MCPForUnity.Editor.Helpers;
3+
using NUnit.Framework;
4+
using UnityEngine;
5+
6+
namespace Fusion
7+
{
8+
public struct NetworkBehaviourBuffer
9+
{
10+
public int Value;
11+
}
12+
13+
public struct Changed<T>
14+
{
15+
public T Value;
16+
}
17+
}
18+
19+
namespace MCPForUnityTests.Editor.Tools
20+
{
21+
public class FusionUnsafeTypeSerializationTests
22+
{
23+
[Test]
24+
public void GetComponentData_SkipsFusionUnsafeTypesInsideContainers()
25+
{
26+
var testObject = new GameObject("FusionUnsafeTypeTestObject");
27+
28+
try
29+
{
30+
var component = testObject.AddComponent<FusionUnsafeTypeComponent>();
31+
component.bufferList.Add(new Fusion.NetworkBehaviourBuffer { Value = 1 });
32+
component.nestedChangedLookup["changed"] = new List<Fusion.Changed<int>>
33+
{
34+
new Fusion.Changed<int> { Value = 2 }
35+
};
36+
component.ChangedListProperty.Add(new Fusion.Changed<int> { Value = 3 });
37+
38+
var result = GameObjectSerializer.GetComponentData(component) as Dictionary<string, object>;
39+
40+
Assert.IsNotNull(result, "GetComponentData should return dictionary data.");
41+
Assert.IsTrue(result.TryGetValue("properties", out object propertiesObject), "Serialized data should contain properties.");
42+
43+
var properties = propertiesObject as Dictionary<string, object>;
44+
Assert.IsNotNull(properties, "Serialized properties should be a dictionary.");
45+
46+
Assert.IsTrue(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.safeValue)), "Safe fields should still serialize.");
47+
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.directBuffer)), "Direct Fusion buffer fields should be skipped.");
48+
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.bufferList)), "Collections containing Fusion buffer types should be skipped.");
49+
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.nestedChangedLookup)), "Nested generic containers containing Fusion Changed<T> should be skipped.");
50+
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.ChangedListProperty)), "Properties returning collections of Fusion Changed<T> should be skipped.");
51+
}
52+
finally
53+
{
54+
UnityEngine.Object.DestroyImmediate(testObject);
55+
}
56+
}
57+
}
58+
59+
public sealed class FusionUnsafeTypeComponent : MonoBehaviour
60+
{
61+
public string safeValue = "kept";
62+
public Fusion.NetworkBehaviourBuffer directBuffer;
63+
public List<Fusion.NetworkBehaviourBuffer> bufferList = new List<Fusion.NetworkBehaviourBuffer>();
64+
public Dictionary<string, List<Fusion.Changed<int>>> nestedChangedLookup = new Dictionary<string, List<Fusion.Changed<int>>>();
65+
66+
public List<Fusion.Changed<int>> ChangedListProperty { get; } = new List<Fusion.Changed<int>>();
67+
}
68+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/FusionUnsafeTypeSerializationTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)