using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine; namespace Unity.Netcode { /// /// Interface used by NetworkVariables to serialize them /// /// internal interface INetworkVariableSerializer { // Write has to be taken by ref here because of INetworkSerializable // Open Instance Delegates (pointers to methods without an instance attached to them) // require the first parameter passed to them (the instance) to be passed by ref. // So foo.Bar() becomes BarDelegate(ref foo); // Taking T as an in parameter like we do in other places would require making a copy // of it to pass it as a ref parameter. public void Write(FastBufferWriter writer, ref T value); public void Read(FastBufferReader reader, out T value); } /// /// Basic serializer for unmanaged types. /// This covers primitives, built-in unity types, and IForceSerializeByMemcpy /// Since all of those ultimately end up calling WriteUnmanagedSafe, this simplifies things /// by calling that directly - thus preventing us from having to have a specific T that meets /// the specific constraints that the various generic WriteValue calls require. /// /// internal class UnmanagedTypeSerializer : INetworkVariableSerializer where T : unmanaged { public void Write(FastBufferWriter writer, ref T value) { writer.WriteUnmanagedSafe(value); } public void Read(FastBufferReader reader, out T value) { reader.ReadUnmanagedSafe(out value); } } /// /// Serializer for FixedStrings, which does the same thing FastBufferWriter/FastBufferReader do, /// but is implemented to get the data it needs using open instance delegates that are passed in /// via reflection. This prevents needing T to meet any interface requirements (which isn't achievable /// without incurring GC allocs on every call to Write or Read - reflection + Open Instance Delegates /// circumvent that.) /// /// Tests show that calling these delegates doesn't cause any GC allocations even though they're /// obtained via reflection and Delegate.CreateDelegate() and called on types that, at compile time, /// aren't known to actually contain those methods. /// /// internal class FixedStringSerializer : INetworkVariableSerializer where T : unmanaged { internal delegate int GetLengthDelegate(ref T value); internal delegate void SetLengthDelegate(ref T value, int length); internal unsafe delegate byte* GetUnsafePtrDelegate(ref T value); internal GetLengthDelegate GetLength; internal SetLengthDelegate SetLength; internal GetUnsafePtrDelegate GetUnsafePtr; public unsafe void Write(FastBufferWriter writer, ref T value) { int length = GetLength(ref value); byte* data = GetUnsafePtr(ref value); writer.WriteUnmanagedSafe(length); writer.WriteBytesSafe(data, length); } public unsafe void Read(FastBufferReader reader, out T value) { value = new T(); reader.ReadValueSafe(out int length); SetLength(ref value, length); reader.ReadBytesSafe(GetUnsafePtr(ref value), length); } } /// /// Serializer for INetworkSerializable types, which does the same thing /// FastBufferWriter/FastBufferReader do, but is implemented to call the NetworkSerialize() method /// via open instance delegates passed in via reflection. This prevents needing T to meet any interface /// requirements (which isn't achievable without incurring GC allocs on every call to Write or Read - /// reflection + Open Instance Delegates circumvent that.) /// /// Tests show that calling these delegates doesn't cause any GC allocations even though they're /// obtained via reflection and Delegate.CreateDelegate() and called on types that, at compile time, /// aren't known to actually contain those methods. /// /// internal class NetworkSerializableSerializer : INetworkVariableSerializer where T : unmanaged { internal delegate void WriteValueDelegate(ref T value, BufferSerializer serializer); internal delegate void ReadValueDelegate(ref T value, BufferSerializer serializer); internal WriteValueDelegate WriteValue; internal ReadValueDelegate ReadValue; public void Write(FastBufferWriter writer, ref T value) { var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); WriteValue(ref value, bufferSerializer); } public void Read(FastBufferReader reader, out T value) { value = new T(); var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); ReadValue(ref value, bufferSerializer); } } /// /// This class is used to register user serialization with NetworkVariables for types /// that are serialized via user serialization, such as with FastBufferReader and FastBufferWriter /// extension methods. Finding those methods isn't achievable efficiently at runtime, so this allows /// users to tell NetworkVariable about those extension methods (or simply pass in a lambda) /// /// public class UserNetworkVariableSerialization { /// /// The write value delegate handler definition /// /// The to write the value of type `T` /// The value of type `T` to be written public delegate void WriteValueDelegate(FastBufferWriter writer, in T value); /// /// The read value delegate handler definition /// /// The to read the value of type `T` /// The value of type `T` to be read public delegate void ReadValueDelegate(FastBufferReader reader, out T value); /// /// The delegate handler declaration /// public static WriteValueDelegate WriteValue; /// /// The delegate handler declaration /// public static ReadValueDelegate ReadValue; } /// /// This class is instantiated for types that we can't determine ahead of time are serializable - types /// that don't meet any of the constraints for methods that are available on FastBufferReader and /// FastBufferWriter. These types may or may not be serializable through extension methods. To ensure /// the user has time to pass in the delegates to UserNetworkVariableSerialization, the existence /// of user serialization isn't checked until it's used, so if no serialization is provided, this /// will throw an exception when an object containing the relevant NetworkVariable is spawned. /// /// internal class FallbackSerializer : INetworkVariableSerializer { public void Write(FastBufferWriter writer, ref T value) { if (UserNetworkVariableSerialization.ReadValue == null || UserNetworkVariableSerialization.WriteValue == null) { throw new ArgumentException($"Type {typeof(T).FullName} is not supported by {typeof(NetworkVariable<>).Name}. If this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list. If not, assign serialization code to {nameof(UserNetworkVariableSerialization)}.{nameof(UserNetworkVariableSerialization.WriteValue)} and {nameof(UserNetworkVariableSerialization)}.{nameof(UserNetworkVariableSerialization.ReadValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}."); } UserNetworkVariableSerialization.WriteValue(writer, value); } public void Read(FastBufferReader reader, out T value) { if (UserNetworkVariableSerialization.ReadValue == null || UserNetworkVariableSerialization.WriteValue == null) { throw new ArgumentException($"Type {typeof(T).FullName} is not supported by {typeof(NetworkVariable<>).Name}. If this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list. If not, assign serialization code to {nameof(UserNetworkVariableSerialization)}.{nameof(UserNetworkVariableSerialization.WriteValue)} and {nameof(UserNetworkVariableSerialization)}.{nameof(UserNetworkVariableSerialization.ReadValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}."); } UserNetworkVariableSerialization.ReadValue(reader, out value); } } internal static class NetworkVariableSerializationTypes { internal static readonly HashSet BaseSupportedTypes = new HashSet { typeof(bool), typeof(byte), typeof(sbyte), typeof(char), typeof(decimal), typeof(double), typeof(float), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(short), typeof(ushort), typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D) }; } /// /// Support methods for reading/writing NetworkVariables /// Because there are multiple overloads of WriteValue/ReadValue based on different generic constraints, /// but there's no way to achieve the same thing with a class, this sets up various read/write schemes /// based on which constraints are met by `T` using reflection, which is done at module load time. /// /// The type the associated NetworkVariable is templated on [Serializable] public static class NetworkVariableSerialization where T : unmanaged { private static INetworkVariableSerializer s_Serializer = GetSerializer(); private static INetworkVariableSerializer GetSerializer() { if (NetworkVariableSerializationTypes.BaseSupportedTypes.Contains(typeof(T))) { return new UnmanagedTypeSerializer(); } if (typeof(INetworkSerializeByMemcpy).IsAssignableFrom(typeof(T))) { return new UnmanagedTypeSerializer(); } if (typeof(Enum).IsAssignableFrom(typeof(T))) { return new UnmanagedTypeSerializer(); } if (typeof(INetworkSerializable).IsAssignableFrom(typeof(T))) { // Obtains "Open Instance Delegates" for the type's NetworkSerialize() methods - // one for an instance of the generic method taking BufferSerializerWriter as T, // one for an instance of the generic method taking BufferSerializerReader as T var writeMethod = (NetworkSerializableSerializer.WriteValueDelegate)Delegate.CreateDelegate(typeof(NetworkSerializableSerializer.WriteValueDelegate), null, typeof(T).GetMethod(nameof(INetworkSerializable.NetworkSerialize)).MakeGenericMethod(typeof(BufferSerializerWriter))); var readMethod = (NetworkSerializableSerializer.ReadValueDelegate)Delegate.CreateDelegate(typeof(NetworkSerializableSerializer.ReadValueDelegate), null, typeof(T).GetMethod(nameof(INetworkSerializable.NetworkSerialize)).MakeGenericMethod(typeof(BufferSerializerReader))); return new NetworkSerializableSerializer { WriteValue = writeMethod, ReadValue = readMethod }; } if (typeof(IUTF8Bytes).IsAssignableFrom(typeof(T)) && typeof(INativeList).IsAssignableFrom(typeof(T))) { // Get "OpenInstanceDelegates" for the Length property (get and set, which are prefixed // with "get_" and "set_" under the hood and emitted as methods) and GetUnsafePtr() var getLength = (FixedStringSerializer.GetLengthDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.GetLengthDelegate), null, typeof(T).GetMethod("get_" + nameof(INativeList.Length))); var setLength = (FixedStringSerializer.SetLengthDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.SetLengthDelegate), null, typeof(T).GetMethod("set_" + nameof(INativeList.Length))); var getUnsafePtr = (FixedStringSerializer.GetUnsafePtrDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.GetUnsafePtrDelegate), null, typeof(T).GetMethod(nameof(IUTF8Bytes.GetUnsafePtr))); return new FixedStringSerializer { GetLength = getLength, SetLength = setLength, GetUnsafePtr = getUnsafePtr }; } return new FallbackSerializer(); } internal static void Write(FastBufferWriter writer, ref T value) { s_Serializer.Write(writer, ref value); } internal static void Read(FastBufferReader reader, out T value) { s_Serializer.Read(reader, out value); } } }