diff --git a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs index 7f160205d77..c6e24003bfd 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs @@ -15,16 +15,79 @@ public sealed partial class DataRecordTest : OurSerializationTest public partial record TwoIntRecord(int aTest, int AnotherTest); [DataRecord] - public partial record OneByteOneDefaultIntRecord(byte A, int B = 5); + public partial record struct TwoIntRecordStruct(int aTest, int AnotherTest); [DataRecord] - public partial record OneLongRecord(long A); + public partial record PrimitiveRecord( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneLongDefaultRecord(long A = 5); + public partial record struct PrimitiveRecordStruct( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneULongRecord(ulong A); + public partial record PrimitiveDefaultsRecord( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); + + [DataRecord] + public partial record struct PrimitiveDefaultsRecordStruct( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); [PrototypeRecord("emptyTestPrototypeRecord")] public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototype; @@ -32,6 +95,9 @@ public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototy [DataRecord] public partial record IntStructHolder(IntStruct Struct); + [DataRecord] + public partial record struct IntStructHolderStruct(IntStruct Struct); + [DataDefinition] public partial struct IntStruct { @@ -46,6 +112,9 @@ public IntStruct(int value) [DataRecord] public partial record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2); + [DataRecord] + public partial record struct TwoIntStructHolderStruct(IntStruct Struct1, IntStruct Struct2); + [DataRecord] public partial record struct DataRecordStruct(IntStruct Struct, string String, int Integer); @@ -58,6 +127,18 @@ public partial record struct DataRecordWithProperties public float X => Position.X; } + [DataRecord] + public partial record struct DataRecordWithDefaultFields() + { + public int A = 1; + } + + [DataRecord] + public partial record struct DataRecordWithDefaultFields2(int A = 1) + { + public int B = 2; + } + [DataRecord] public readonly partial record struct ReadonlyDataRecord { @@ -98,61 +179,178 @@ public void TwoIntRecordTest() } [Test] - public void OneByteOneDefaultIntRecordTest() + public void TwoIntRecordStructTest() { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"aTest", "1"}, + {"anotherTest", "2"} + }; + + var val = Serialization.Read(mapping); Assert.Multiple(() => { - Assert.That(val.A, Is.EqualTo(1)); - Assert.That(val.B, Is.EqualTo(5)); + Assert.That(val.aTest, Is.EqualTo(1)); + Assert.That(val.AnotherTest, Is.EqualTo(2)); }); - } - [Test] - public void OneLongRecordTest() - { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var newMapping = Serialization.WriteValueAs(val); - Assert.That(val.A, Is.EqualTo(1)); - } + Assert.Multiple(() => + { + Assert.That(newMapping, Has.Count.EqualTo(2)); - [Test] - public void OneLongMinValueRecordTest() - { - var mapping = new MappingDataNode {{"a", long.MinValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + Assert.That(newMapping.TryGet("aTest", out var aTestNode)); + Assert.That(aTestNode!.Value, Is.EqualTo("1")); - Assert.That(val.A, Is.EqualTo(long.MinValue)); + Assert.That(newMapping.TryGet("anotherTest", out var anotherTestNode)); + Assert.That(anotherTestNode!.Value, Is.EqualTo("2")); + }); } [Test] - public void OneLongMaxValueRecordTest() + public void PrimitiveRecordTest() { - var mapping = new MappingDataNode {{"a", long.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode(); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(long.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(false)); + Assert.That(val2.Bool, Is.EqualTo(false)); + Assert.That(val1.Byte, Is.EqualTo(0)); + Assert.That(val2.Byte, Is.EqualTo(0)); + Assert.That(val1.Sbyte, Is.EqualTo(0)); + Assert.That(val2.Sbyte, Is.EqualTo(0)); + Assert.That(val1.Char, Is.EqualTo(default(char))); + Assert.That(val2.Char, Is.EqualTo(default(char))); + //Assert.That(val1.Decimal, Is.EqualTo(0)); + //Assert.That(val2.Decimal, Is.EqualTo(0)); + Assert.That(val1.Double, Is.EqualTo(0)); + Assert.That(val2.Double, Is.EqualTo(0)); + Assert.That(val1.Float, Is.EqualTo(0)); + Assert.That(val2.Float, Is.EqualTo(0)); + Assert.That(val1.Int, Is.EqualTo(0)); + Assert.That(val2.Int, Is.EqualTo(0)); + Assert.That(val1.Uint, Is.EqualTo(0)); + Assert.That(val2.Uint, Is.EqualTo(0)); + Assert.That(val1.Nint, Is.EqualTo((nint) 0)); + Assert.That(val2.Nint, Is.EqualTo((nint) 0)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val1.Long, Is.EqualTo(0)); + Assert.That(val2.Long, Is.EqualTo(0)); + Assert.That(val1.Ulong, Is.EqualTo(0)); + Assert.That(val2.Ulong, Is.EqualTo(0)); + Assert.That(val1.Short, Is.EqualTo(0)); + Assert.That(val2.Short, Is.EqualTo(0)); + Assert.That(val1.UShort, Is.EqualTo(0)); + Assert.That(val2.UShort, Is.EqualTo(0)); + }); } [Test] - public void OneLongDefaultRecordTest() + public void PrimitiveDefaultsRecordTest() { var mapping = new MappingDataNode(); - var val = Serialization.Read(mapping, notNullableOverride: true); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(5)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val1.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val2.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] - public void OneULongRecordMaxValueTest() + public void PrimitiveRecordMinMaxValueTest() { - var mapping = new MappingDataNode {{"a", ulong.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"bool", "true"}, + {"byte", byte.MaxValue.ToString()}, + {"sbyte", sbyte.MinValue.ToString()}, + {"char", "A"}, + //{"decimal", "-1"}, + {"double", "-1"}, + {"float", "-1"}, + {"int", int.MinValue.ToString()}, + {"uint", uint.MaxValue.ToString()}, + // TODO SERIALIZATION add nint yaml serializer? + //{"nint", nint.MinValue.ToString()}, + //{"nuint", nuint.MinValue.ToString()}, + {"long", long.MinValue.ToString()}, + {"ulong", ulong.MaxValue.ToString()}, + {"short", short.MinValue.ToString()}, + {"ushort", ushort.MaxValue.ToString()}, + }; + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(ulong.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + //Assert.That(val1.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val2.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val1.Nuint, Is.EqualTo(nuint.MaxValue)); + //Assert.That(val2.Nuint, Is.EqualTo(nuint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] @@ -164,6 +362,18 @@ public void PrototypeTest() Assert.That(val.ID, Is.EqualTo("ABC")); } + [Test] + public void DataRecordWithDefaultFieldsTest() + { + var mapping = new MappingDataNode (); + var val = Serialization.Read(mapping); + Assert.That(val.A, Is.EqualTo(1)); + + var val2 = Serialization.Read(mapping); + Assert.That(val2.A, Is.EqualTo(1)); + Assert.That(val2.B, Is.EqualTo(2)); + } + [Test] public void RegisterPrototypeTest() { @@ -186,8 +396,10 @@ public void IntStructHolderTest() } }; var val = Serialization.Read(mapping, notNullableOverride: true); + var structVal = Serialization.Read(mapping); Assert.That(val.Struct.Value, Is.EqualTo(42)); + Assert.That(structVal.Struct.Value, Is.EqualTo(42)); } [Test] diff --git a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs index d9f62d1cc46..5c5771f902e 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs @@ -9,19 +9,56 @@ internal sealed partial class DataStructTest : OurSerializationTest [DataDefinition] public partial struct DefaultIntDataStruct { + [DataField] public int A = 5; + [DataField] + public int B; + public DefaultIntDataStruct() { + B = 1; } } + [DataDefinition] + public partial struct DefaultIntDataStructNoConstructor + { + [DataField] + public int A = 5; + + [DataField] + public int B; + } + [Test] public void DefaultIntDataStructTest() { var mapping = new MappingDataNode(); var val = Serialization.Read(mapping); + var val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"a", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(10)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(10)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"b", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(10)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(10)); } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs index 4202e9494cb..17e701d3017 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs @@ -47,11 +47,21 @@ private static void CreateClassInstantiator(ILGenerator generator, Type type) generator.Emit(OpCodes.Ret); } + /// + /// This generates IL code that will try to invoke a record's constructor by passing default values to any arguments. + /// private static void CreateRecordInstantiator(ILGenerator generator, Type type) { var constructors = type.GetConstructors(); if (constructors.Length == 0) - throw new ArgumentException($"Could not find a constructor for record class {type}"); + { + if (!type.IsValueType) + throw new ArgumentException($"Could not find a constructor for record class {type}"); + + // Handle constructorless data record struct by treating it like a normal struct + CreateValueTypeInstantiator(generator, type); + return; + } var constructor = constructors[0]; foreach (var parameter in constructor.GetParameters()) @@ -60,11 +70,61 @@ private static void CreateRecordInstantiator(ILGenerator generator, Type type) if (parameterType.IsPrimitive) { - var defaultValue = Convert.ToInt64(parameter.HasDefaultValue ? parameter.DefaultValue! : 0); - generator.Emit(OpCodes.Ldc_I4, defaultValue); - if (parameterType == typeof(long) || parameterType == typeof(ulong)) - generator.Emit(OpCodes.Conv_I8); + if (parameterType == typeof(decimal)) + { + // I CBF figuring out how to support them, so fuck it. + throw new NotSupportedException($"Record class {type} contains decimals. DataRecords don't currently support decimals."); + // If anyone wants to try, a value of 0 looks like this in IL: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::Zero + // While a default value of -1 uses another static field: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::MinusOne + } + + if (parameterType == typeof(float)) + { + var floatDefault = parameter.HasDefaultValue ? (float) parameter.DefaultValue! : 0f; + generator.Emit(OpCodes.Ldc_R4, floatDefault); + } + else if (parameterType == typeof(double)) + { + var doubleDefault = parameter.HasDefaultValue ? (double) parameter.DefaultValue! : 0d; + generator.Emit(OpCodes.Ldc_R8, doubleDefault); + } + else if (parameterType == typeof(nint) || parameterType == typeof(nuint)) + { + int nintDefault = parameter.HasDefaultValue ? (int)Convert.ToInt64(parameter.DefaultValue) : 0; + generator.Emit(OpCodes.Ldc_I4, nintDefault); + + if (parameterType == typeof(nuint) && nintDefault < 0) // I'm only like 50% sure this is correct, but it makes the tests pass, so.... + generator.Emit(OpCodes.Conv_U); + else + generator.Emit(OpCodes.Conv_I); + } + else if (parameterType == typeof(long) || parameterType == typeof(ulong)) + { + var longDefault = 0L; + if (parameter.HasDefaultValue) + { + longDefault = parameterType == typeof(ulong) + ? (long) (ulong) parameter.DefaultValue! + : Convert.ToInt64(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I8, longDefault); + } + else + { + var intDefault = 0; + if (parameter.HasDefaultValue) + { + intDefault = parameterType == typeof(uint) + ? (int) (uint) parameter.DefaultValue! + : Convert.ToInt32(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I4, intDefault); + } } else if (parameterType.IsValueType) { @@ -104,13 +164,13 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< var generator = method.GetILGenerator(); - if (type.IsValueType) + if (isRecord) { - CreateValueTypeInstantiator(generator, type); + CreateRecordInstantiator(generator, type); } - else if (isRecord) + else if (type.IsValueType) { - CreateRecordInstantiator(generator, type); + CreateValueTypeInstantiator(generator, type); } else {