Handle generic PPtr fields in MonoBehaviour deserialization

* Resolves #1226
This commit is contained in:
ds5678 2025-05-09 23:26:18 -07:00
parent 09ed03b735
commit 9c079d0a95
5 changed files with 299 additions and 234 deletions

View File

@ -0,0 +1,209 @@
namespace AssetRipper.SerializationLogic.Tests;
public class CyclicalReferenceTests
{
[Serializable]
private class SelfReferencingClass
{
public SelfReferencingClass? selfReference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D1()
{
SerializableType serializableType = SerializableTypes.Create<SelfReferencingClass>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(0)); // Infinite recursion disqualifies a field from serialization
}
[Serializable]
private class CyclicalReferenceClass_C1_D2
{
public CyclicalReferenceClass_C2_D2? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D2
{
public CyclicalReferenceClass_C1_D2? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D2()
{
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D2>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3
{
public CyclicalReferenceClass_C2_D3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3
{
public CyclicalReferenceClass_C3_D3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3
{
public CyclicalReferenceClass_C1_D3? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3()
{
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D3>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D4
{
public CyclicalReferenceClass_C2_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D4
{
public CyclicalReferenceClass_C3_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C3_D4
{
public CyclicalReferenceClass_C4_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C4_D4
{
public CyclicalReferenceClass_C1_D4? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D4()
{
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D4>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V1
{
public CyclicalReferenceClass_C2_D3_V1? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V1
{
public CyclicalReferenceClass_C3_D3_V1? reference1;
public CyclicalReferenceClass_C3_D3_V1? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V1
{
public CyclicalReferenceClass_C1_D3_V1? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V1()
{
// Variant 1: Two references to the same class
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D3_V1>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V2
{
public CyclicalReferenceClass_C2_D3_V2? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V2
{
public CyclicalReferenceClass_C3_D3_V2? reference1;
public CyclicalReferenceClass_C1_D3_V2? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V2
{
public CyclicalReferenceClass_C1_D3_V2? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V2()
{
// Variant 2: Reference to child class, then reference to parent class
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D3_V2>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V3
{
public CyclicalReferenceClass_C2_D3_V3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V3
{
public CyclicalReferenceClass_C1_D3_V3? reference1;
public CyclicalReferenceClass_C3_D3_V3? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V3
{
public CyclicalReferenceClass_C1_D3_V3? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V3()
{
// Variant 3: Reference to parent class, then reference to child class
List<SerializableType> serializableType = SerializableTypes.CreateMultiple<CyclicalReferenceClass_C1_D3_V3>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
}

View File

@ -1,5 +1,3 @@
using AssetRipper.Primitives;
namespace AssetRipper.SerializationLogic.Tests;
public class FieldSerializationTests
@ -15,7 +13,7 @@ public class FieldSerializationTests
[Test]
public void PrivateFieldsAreCorrectlyDiscriminated()
{
SerializableType serializableType = CreateSerializableType<CustomMonoBehaviourWithPrivateFields>();
SerializableType serializableType = SerializableTypes.Create<CustomMonoBehaviourWithPrivateFields>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(1));
SerializableType.Field field = serializableType.Fields[0];
Assert.That(field.Name, Is.EqualTo("field1"));
@ -32,7 +30,7 @@ public class FieldSerializationTests
[Test]
public void PublicFieldsAreCorrectlyDiscriminated()
{
SerializableType serializableType = CreateSerializableType<CustomMonoBehaviourWithPublicFields>();
SerializableType serializableType = SerializableTypes.Create<CustomMonoBehaviourWithPublicFields>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(1));
SerializableType.Field field = serializableType.Fields[0];
Assert.That(field.Name, Is.EqualTo(nameof(CustomMonoBehaviourWithPublicFields.field2)));
@ -46,7 +44,7 @@ public class FieldSerializationTests
[Test]
public void ResolutionForUnityTypesWorksAsExpected()
{
SerializableType serializableType = CreateSerializableType<CustomMonoBehaviourWithListField>();
SerializableType serializableType = SerializableTypes.Create<CustomMonoBehaviourWithListField>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(1));
SerializableType.Field field = serializableType.Fields[0];
using (Assert.EnterMultipleScope())
@ -71,7 +69,7 @@ public class FieldSerializationTests
[Test]
public void DeserializationSupportsGenericTypes()
{
SerializableType serializableType = CreateSerializableType<CustomMonoBehaviourWithGenericField>();
SerializableType serializableType = SerializableTypes.Create<CustomMonoBehaviourWithGenericField>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(1));
SerializableType.Field field = serializableType.Fields[0];
@ -91,248 +89,47 @@ public class FieldSerializationTests
}
}
[Serializable]
private class SelfReferencingClass
private class CustomMonoBehaviourWithObjectField : UnityEngine.MonoBehaviour
{
public SelfReferencingClass? selfReference;
public UnityEngine.Object? pptr;
}
[Test]
public void CyclicalReferenceClassIsHandled_D1()
private class CustomMonoBehaviourWithMonoBehaviourField : UnityEngine.MonoBehaviour
{
SerializableType serializableType = CreateSerializableType<SelfReferencingClass>();
Assert.That(serializableType.Fields, Has.Count.EqualTo(0)); // Infinite recursion disqualifies a field from serialization
public UnityEngine.Object? pptr;
}
[Serializable]
private class CyclicalReferenceClass_C1_D2
private abstract class GenericAbstractMonoBehaviour<T> : UnityEngine.MonoBehaviour
{
public CyclicalReferenceClass_C2_D2? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D2
private class NonGenericDerivedMonoBehaviour : GenericAbstractMonoBehaviour<int>
{
public CyclicalReferenceClass_C1_D2? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D2()
private class CustomMonoBehaviourWithNonGenericDerivedMonoBehaviourField : UnityEngine.MonoBehaviour
{
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D2>();
public NonGenericDerivedMonoBehaviour? pptr;
}
private class CustomMonoBehaviourWithGenericAbstractMonoBehaviourField : UnityEngine.MonoBehaviour
{
public GenericAbstractMonoBehaviour<int>? pptr;
}
[TestCase(typeof(CustomMonoBehaviourWithObjectField))]
[TestCase(typeof(CustomMonoBehaviourWithMonoBehaviourField))]
[TestCase(typeof(CustomMonoBehaviourWithNonGenericDerivedMonoBehaviourField))]
[TestCase(typeof(CustomMonoBehaviourWithGenericAbstractMonoBehaviourField))]
public void DeserializationSupportsPPtrFields(Type type)
{
SerializableType serializableType = SerializableTypes.Create(type);
Assert.That(serializableType.Fields, Has.Count.EqualTo(1));
SerializableType.Field field = serializableType.Fields[0];
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
Assert.That(field.Type, Is.EqualTo(SerializablePointerType.Shared));
Assert.That(field.ArrayDepth, Is.EqualTo(0));
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3
{
public CyclicalReferenceClass_C2_D3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3
{
public CyclicalReferenceClass_C3_D3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3
{
public CyclicalReferenceClass_C1_D3? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3()
{
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D3>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D4
{
public CyclicalReferenceClass_C2_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D4
{
public CyclicalReferenceClass_C3_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C3_D4
{
public CyclicalReferenceClass_C4_D4? reference;
}
[Serializable]
private class CyclicalReferenceClass_C4_D4
{
public CyclicalReferenceClass_C1_D4? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D4()
{
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D4>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V1
{
public CyclicalReferenceClass_C2_D3_V1? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V1
{
public CyclicalReferenceClass_C3_D3_V1? reference1;
public CyclicalReferenceClass_C3_D3_V1? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V1
{
public CyclicalReferenceClass_C1_D3_V1? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V1()
{
// Variant 1: Two references to the same class
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D3_V1>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V2
{
public CyclicalReferenceClass_C2_D3_V2? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V2
{
public CyclicalReferenceClass_C3_D3_V2? reference1;
public CyclicalReferenceClass_C1_D3_V2? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V2
{
public CyclicalReferenceClass_C1_D3_V2? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V2()
{
// Variant 2: Reference to child class, then reference to parent class
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D3_V2>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
[Serializable]
private class CyclicalReferenceClass_C1_D3_V3
{
public CyclicalReferenceClass_C2_D3_V3? reference;
}
[Serializable]
private class CyclicalReferenceClass_C2_D3_V3
{
public CyclicalReferenceClass_C1_D3_V3? reference1;
public CyclicalReferenceClass_C3_D3_V3? reference2;
}
[Serializable]
private class CyclicalReferenceClass_C3_D3_V3
{
public CyclicalReferenceClass_C1_D3_V3? reference;
}
[Test]
public void CyclicalReferenceClassIsHandled_D3_V3()
{
// Variant 3: Reference to parent class, then reference to child class
List<SerializableType> serializableType = CreateSerializableTypes<CyclicalReferenceClass_C1_D3_V3>();
using (Assert.EnterMultipleScope())
{
foreach (SerializableType type in serializableType)
{
Assert.That(type.Fields, Has.Count.EqualTo(0), $"{type.Name} should have no fields.");
}
}
}
private static SerializableType CreateSerializableType<T>() => CreateSerializableType<T>(DefaultUnityVersion);
private static SerializableType CreateSerializableType<T>(UnityVersion version)
{
TypeDefinition typeDefinition = ReferenceAssemblies.GetType<T>();
FieldSerializer serializer = new(version);
if (serializer.TryCreateSerializableType(typeDefinition, out SerializableType? serializableType, out string? failureReason))
{
return serializableType;
}
else
{
Assert.Fail($"Failed to create serializable type: {failureReason}");
return default!;
}
}
private static List<SerializableType> CreateSerializableTypes<T>() => CreateSerializableTypes<T>(DefaultUnityVersion);
private static List<SerializableType> CreateSerializableTypes<T>(UnityVersion version)
{
TypeDefinition typeDefinition = ReferenceAssemblies.GetType<T>();
FieldSerializer serializer = new(version);
Dictionary<ITypeDefOrRef, SerializableType> typeCache = new();
if (serializer.TryCreateSerializableType(typeDefinition, typeCache, out SerializableType? serializableType, out string? failureReason))
{
return typeCache.Values.ToList();
}
else
{
Assert.Fail($"Failed to create serializable type: {failureReason}");
return default!;
}
}
/// <summary>
/// Assume a recent Unity version if not specified.
/// </summary>
private static UnityVersion DefaultUnityVersion => new(6000);
}

View File

@ -0,0 +1,48 @@
using AssetRipper.Primitives;
namespace AssetRipper.SerializationLogic.Tests;
public static class SerializableTypes
{
public static SerializableType Create<T>() => Create<T>(DefaultUnityVersion);
public static SerializableType Create<T>(UnityVersion version) => Create(typeof(T), version);
public static SerializableType Create(Type type) => Create(type, DefaultUnityVersion);
public static SerializableType Create(Type type, UnityVersion version)
{
TypeDefinition typeDefinition = ReferenceAssemblies.GetType(type);
FieldSerializer serializer = new(version);
if (serializer.TryCreateSerializableType(typeDefinition, out SerializableType? serializableType, out string? failureReason))
{
return serializableType;
}
else
{
Assert.Fail($"Failed to create serializable type: {failureReason}");
return default!;
}
}
public static List<SerializableType> CreateMultiple<T>() => CreateMultiple<T>(DefaultUnityVersion);
public static List<SerializableType> CreateMultiple<T>(UnityVersion version) => CreateMultiple(typeof(T), version);
public static List<SerializableType> CreateMultiple(Type type) => CreateMultiple(type, DefaultUnityVersion);
public static List<SerializableType> CreateMultiple(Type type, UnityVersion version)
{
TypeDefinition typeDefinition = ReferenceAssemblies.GetType(type);
FieldSerializer serializer = new(version);
Dictionary<ITypeDefOrRef, SerializableType> typeCache = new();
if (serializer.TryCreateSerializableType(typeDefinition, typeCache, out SerializableType? serializableType, out string? failureReason))
{
return typeCache.Values.ToList();
}
else
{
Assert.Fail($"Failed to create serializable type: {failureReason}");
return default!;
}
}
/// <summary>
/// Assume a recent Unity version if not specified.
/// </summary>
private static UnityVersion DefaultUnityVersion => new(6000);
}

View File

@ -51,6 +51,11 @@ namespace AssetRipper.SerializationLogic.Extensions
// let's assume it is not assignable to our target type
}
public static bool InheritsFromObject(this ITypeDescriptor type)
{
return type.IsAssignableTo("UnityEngine", "Object");
}
public static bool IsEnum(this ITypeDescriptor type)
{
return type.IsValueType && !type.IsPrimitive() && type.CheckedResolve().IsEnum;

View File

@ -241,7 +241,13 @@ public readonly struct FieldSerializer(UnityVersion version)
return TryCreateSerializableField(typeStack, name, szArrayTypeSignature.BaseType, arrayDepth + 1, typeCache, out result, out failureReason);
case GenericInstanceTypeSignature genericInstanceTypeSignature:
if (typeCache.TryGetValue(genericInstanceTypeSignature.ToTypeDefOrRef(), out SerializableType? cachedGenericMonoType))
if (genericInstanceTypeSignature.InheritsFromObject())
{
result = new Field(SerializablePointerType.Shared, arrayDepth, name, true);
failureReason = null;
return true;
}
else if (typeCache.TryGetValue(genericInstanceTypeSignature.ToTypeDefOrRef(), out SerializableType? cachedGenericMonoType))
{
result = new Field(cachedGenericMonoType, arrayDepth, name, true);
failureReason = null;