using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using JetBrains.Annotations; using NetSerializer; using Robust.Shared.GameObjects; namespace Robust.Shared.Serialization; /// /// Generated delta states store ONE bitmask and ONE /// field per networked component field. This serializer writes the mask first, then only writes fields whose bits are /// set. This allows a single generated delta state to represent any dirty-field combination without serializing the /// unchanged fields. /// If you misuse this your networked states will probably explode! /// internal sealed class NetComponentDeltaStateSerializer : IDynamicTypeSerializer { public bool Handles(Type type) { return typeof(IAutoGeneratedComponentDeltaState).IsAssignableFrom(type); } public IEnumerable GetSubtypes(Type type) { yield return typeof(ulong); foreach (var field in GetDeltaFields(type)) { yield return field.FieldType; } } public void GenerateWriterMethod(Serializer serializer, Type type, ILGenerator il) { // arg0: Serializer, arg1: Stream, arg2: value var getChangedFields = typeof(IAutoGeneratedComponentDeltaState) .GetProperty(nameof(IAutoGeneratedComponentDeltaState.ChangedFields))! .GetMethod!; var writeMask = typeof(Primitives) .GetMethod(nameof(Primitives.WritePrimitive), [typeof(Stream), typeof(ulong)])!; var changedFields = il.DeclareLocal(typeof(ulong)); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Castclass, typeof(IAutoGeneratedComponentDeltaState)); il.Emit(OpCodes.Callvirt, getChangedFields); il.Emit(OpCodes.Stloc, changedFields); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldloc, changedFields); il.Emit(OpCodes.Call, writeMask); foreach (var field in GetDeltaFields(type)) { var endField = il.DefineLabel(); var mask = 1UL << field.GetCustomAttribute()!.Index; il.Emit(OpCodes.Ldloc, changedFields); il.Emit(OpCodes.Ldc_I8, unchecked((long) mask)); il.Emit(OpCodes.And); il.Emit(OpCodes.Brfalse, endField); // Primitive types count as sealed // We need this to handle inheritors of abstract types, such as SoundSpecifier var writeField = field.FieldType.IsSealed ? WriteFieldMethod.MakeGenericMethod(field.FieldType) : WriteFieldObjectMethod; il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Ldfld, field); il.Emit(OpCodes.Call, writeField); il.MarkLabel(endField); } il.Emit(OpCodes.Ret); } public void GenerateReaderMethod(Serializer serializer, Type type, ILGenerator il) { // arg0: Serializer, arg1: Stream, arg2: out value var setChangedFields = typeof(IAutoGeneratedComponentDeltaState) .GetProperty(nameof(IAutoGeneratedComponentDeltaState.ChangedFields))! .SetMethod!; var readMask = typeof(Primitives) .GetMethod(nameof(Primitives.ReadPrimitive), [typeof(Stream), typeof(ulong).MakeByRefType()])!; var changedFields = il.DeclareLocal(typeof(ulong)); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Ldtoken, type); il.Emit(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), BindingFlags.Public | BindingFlags.Static)!); il.Emit( OpCodes.Call, // Beats me but matches the other serializers. #pragma warning disable SYSLIB0050 typeof(System.Runtime.Serialization.FormatterServices) .GetMethod(nameof(System.Runtime.Serialization.FormatterServices.GetUninitializedObject), BindingFlags.Public | BindingFlags.Static)!); #pragma warning restore SYSLIB0050 il.Emit(OpCodes.Castclass, type); il.Emit(OpCodes.Stind_Ref); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldloca_S, changedFields); il.Emit(OpCodes.Call, readMask); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Ldind_Ref); il.Emit(OpCodes.Castclass, typeof(IAutoGeneratedComponentDeltaState)); il.Emit(OpCodes.Ldloc, changedFields); il.Emit(OpCodes.Callvirt, setChangedFields); foreach (var field in GetDeltaFields(type)) { var endField = il.DefineLabel(); var mask = 1UL << field.GetCustomAttribute()!.Index; il.Emit(OpCodes.Ldloc, changedFields); il.Emit(OpCodes.Ldc_I8, unchecked((long) mask)); il.Emit(OpCodes.And); il.Emit(OpCodes.Brfalse, endField); // Primitive types count as sealed // We need this to handle inheritors of abstract types, such as SoundSpecifier var readField = field.FieldType.IsSealed ? ReadFieldMethod.MakeGenericMethod(field.FieldType) : ReadFieldObjectMethod; il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Ldind_Ref); il.Emit(OpCodes.Ldflda, field); il.Emit(OpCodes.Call, readField); il.MarkLabel(endField); } il.Emit(OpCodes.Ret); } private static IEnumerable GetDeltaFields(Type type) { return type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(field => (Field: field, Attribute: field.GetCustomAttribute())) .Where(tuple => tuple.Attribute != null) .OrderBy(tuple => tuple.Attribute!.Index) .Select(tuple => tuple.Field); } private static readonly MethodInfo WriteFieldMethod = typeof(NetComponentDeltaStateSerializer) .GetMethod(nameof(WriteField), BindingFlags.Static | BindingFlags.NonPublic)!; private static readonly MethodInfo WriteFieldObjectMethod = typeof(NetComponentDeltaStateSerializer) .GetMethod(nameof(WriteFieldObject), BindingFlags.Static | BindingFlags.NonPublic)!; private static readonly MethodInfo ReadFieldMethod = typeof(NetComponentDeltaStateSerializer) .GetMethod(nameof(ReadField), BindingFlags.Static | BindingFlags.NonPublic)!; private static readonly MethodInfo ReadFieldObjectMethod = typeof(NetComponentDeltaStateSerializer) .GetMethod(nameof(ReadFieldObject), BindingFlags.Static | BindingFlags.NonPublic)!; [UsedImplicitly] private static void WriteField(Serializer serializer, Stream stream, T value) { serializer.SerializeDirect(stream, value); } [UsedImplicitly] private static void WriteFieldObject(Serializer serializer, Stream stream, object value) { serializer.Serialize(stream, value); } [UsedImplicitly] private static void ReadField(Serializer serializer, Stream stream, out T value) { serializer.DeserializeDirect(stream, out value); } [UsedImplicitly] private static void ReadFieldObject(Serializer serializer, Stream stream, out object value) { serializer.Deserialize(stream, out value); } }