diff --git a/Reactor/Networking/Attributes/MethodRpcAttribute.cs b/Reactor/Networking/Attributes/MethodRpcAttribute.cs index b0fa1b09..c39cd6d6 100644 --- a/Reactor/Networking/Attributes/MethodRpcAttribute.cs +++ b/Reactor/Networking/Attributes/MethodRpcAttribute.cs @@ -33,12 +33,6 @@ public sealed class MethodRpcAttribute : Attribute /// public RpcLocalHandling LocalHandling { get; set; } = RpcLocalHandling.Before; - /// - /// Gets or sets a value indicating whether the rpc should be sent immediately. - /// - [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. This property will be removed in a future version.")] - public bool SendImmediately { get; set; } = true; - /// /// Initializes a new instance of the class. /// @@ -56,8 +50,7 @@ public MethodRpcAttribute(uint id) /// The plugin to register the rpc to. public static void Register(Assembly assembly, BasePlugin plugin) { - if (_registeredAssemblies.Contains(assembly)) return; - _registeredAssemblies.Add(assembly); + if (!_registeredAssemblies.Add(assembly)) return; var methods = assembly.GetTypes() .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)); diff --git a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs index 439cf420..f67fcfd0 100644 --- a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs +++ b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.IO; using Hazel; using Hazel.Udp; using Reactor.Networking.Serialization; @@ -21,6 +22,34 @@ private static float ReverseLerp(float t) return Mathf.Clamp01((t - MIN) / DIFF); } + /// + /// Writes a packed long value to the . + /// + /// The to write to. + /// The to write. + public static void WritePacked(this MessageWriter writer, long value) + { + // From what I can tell from the publicly available source of Hazel, they just cast the int value to a uint?? + // Seems like they never heard of ZigZag encoding + writer.WritePacked((ulong) ((value << 1) ^ (value >> 63))); + } + + /// + /// Writes a packed ulong value to the . + /// + /// The to write to. + /// The to write. + public static void WritePacked(this MessageWriter writer, ulong value) + { + while (value >= 0x80) + { + writer.Write((byte) (value | 0x80)); + value >>= 7; + } + + writer.Write((byte) value); + } + /// /// Writes a to the . /// @@ -47,7 +76,17 @@ public static void Write(this MessageWriter writer, Vector2 value) /// /// The to write to. /// The to write. - public static void Write(this MessageWriter writer, long value) => writer.Write(BitConverter.GetBytes(value)); + public static void Write(this MessageWriter writer, long value) + { + writer.Write((byte) value); + writer.Write((byte) (value >> 8)); + writer.Write((byte) (value >> 16)); + writer.Write((byte) (value >> 24)); + writer.Write((byte) (value >> 32)); + writer.Write((byte) (value >> 40)); + writer.Write((byte) (value >> 48)); + writer.Write((byte) (value >> 56)); + } /// /// Writes a color value to the . @@ -62,6 +101,58 @@ public static void Write(this MessageWriter writer, Color32 value) writer.Write(value.a); } + /// + /// Writes a color value to the . + /// + /// The to write to. + /// The to write. + public static void Write(this MessageWriter writer, Color value) + { + writer.Write(value.r); + writer.Write(value.g); + writer.Write(value.b); + writer.Write(value.a); + } + + /// + /// Reads a packed from the . + /// + /// The to read from. + /// A from the . + public static long ReadPackedInt64(this MessageReader reader) + { + // See comment in WritePacked(long) + var val = reader.ReadPackedUInt64(); + return (long) (val >> 1) ^ -(long) (val & 1); + } + + /// + /// Reads a packed from the . + /// + /// The to read from. + /// A from the . + public static ulong ReadPackedUInt64(this MessageReader reader) + { + var result = 0ul; + var shift = 0; + + while (true) + { + var b = reader.ReadByte(); + result |= (ulong) (b & 0x7F) << shift; + + if ((b & 0x80) == 0) + break; + + shift += 7; + + if (shift >= 70) + throw new InvalidDataException("VarInt too long"); + } + + return result; + } + /// /// Reads a from the . /// @@ -96,7 +187,18 @@ public static Vector2 ReadVector2(this MessageReader reader) /// /// The to read from. /// The resulting long value from the . - public static long ReadInt64(this MessageReader reader) => BitConverter.ToInt64(reader.ReadBytes(8), 0); + public static long ReadInt64(this MessageReader reader) + { + var uval = (long) reader.ReadByte(); + uval |= (long) reader.ReadByte() << 8; + uval |= (long) reader.ReadByte() << 16; + uval |= (long) reader.ReadByte() << 24; + uval |= (long) reader.ReadByte() << 32; + uval |= (long) reader.ReadByte() << 40; + uval |= (long) reader.ReadByte() << 48; + uval |= (long) reader.ReadByte() << 56; + return uval; + } /// /// Reads a color value from a network message. diff --git a/Reactor/Networking/Rpc/CustomRpc.cs b/Reactor/Networking/Rpc/CustomRpc.cs index fe161eb0..cb70f556 100644 --- a/Reactor/Networking/Rpc/CustomRpc.cs +++ b/Reactor/Networking/Rpc/CustomRpc.cs @@ -56,11 +56,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data) /// /// The to send the rpc on. /// The data to send. - /// Whether to send it immediately. /// The callback to invoke when this packet is acknowledged. - public void Send(InnerNetObject innerNetObject, TData data, bool immediately = false, Action? ackCallback = null) + public void Send(InnerNetObject innerNetObject, TData data, Action? ackCallback = null) { - UnsafeSend(innerNetObject, data, immediately, ackCallback: ackCallback); + UnsafeSend(innerNetObject, data, ackCallback: ackCallback); } /// @@ -72,7 +71,7 @@ public void Send(InnerNetObject innerNetObject, TData data, bool immediately = f /// The callback to invoke when this packet is acknowledged. public void SendTo(InnerNetObject innerNetObject, int targetClientId, TData data, Action? ackCallback = null) { - UnsafeSend(innerNetObject, data, true, targetClientId, ackCallback); + UnsafeSend(innerNetObject, data, targetClientId, ackCallback); } } @@ -121,11 +120,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data) /// Sends this custom rpc on the specified . /// /// The to send the rpc on. - /// Whether to send it immediately. /// The callback to invoke when this packet is acknowledged. - public void Send(InnerNetObject innerNetObject, bool immediately = false, Action? ackCallback = null) + public void Send(InnerNetObject innerNetObject, Action? ackCallback = null) { - UnsafeSend(innerNetObject, immediately, ackCallback: ackCallback); + UnsafeSend(innerNetObject, null, ackCallback: ackCallback); } /// @@ -136,6 +134,6 @@ public void Send(InnerNetObject innerNetObject, bool immediately = false, Action /// The callback to invoke when this packet is acknowledged. public void SendTo(InnerNetObject innerNetObject, int targetClientId, Action? ackCallback = null) { - UnsafeSend(innerNetObject, null, true, targetClientId, ackCallback); + UnsafeSend(innerNetObject, null, targetClientId, ackCallback); } } diff --git a/Reactor/Networking/Rpc/MethodRpc.cs b/Reactor/Networking/Rpc/MethodRpc.cs index 49016355..3f1bd61a 100644 --- a/Reactor/Networking/Rpc/MethodRpc.cs +++ b/Reactor/Networking/Rpc/MethodRpc.cs @@ -69,22 +69,6 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio _handle = Hook(method, parameters, method.IsStatic); } - /// - /// Initializes a new instance of the class. - /// - /// The plugin that the rpc is attached to. - /// The method of the method rpc. - /// The id of the rpc. - /// The send option of the rpc. - /// The local handling method of the rpc. - /// The value indicating whether the rpc should be sent immediately. - [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. Remove sendImmediately from the parameter list.")] - public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling, bool sendImmediately) - : this(plugin, method, id, option, localHandling) - { - SendImmediately = sendImmediately; - } - /// /// Gets the method of the method rpc. /// @@ -102,12 +86,6 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio /// public override SendOption SendOption { get; } - /// - /// Gets a value indicating whether the method rpc should be sent immediately. - /// - [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. This property will be removed in a future version.")] - public bool SendImmediately { get; } = true; - /// public override void UnsafeWrite(MessageWriter writer, object? data) { diff --git a/Reactor/Networking/Rpc/PlayerCustomRpc.cs b/Reactor/Networking/Rpc/PlayerCustomRpc.cs index 47c9d8a1..77b26847 100644 --- a/Reactor/Networking/Rpc/PlayerCustomRpc.cs +++ b/Reactor/Networking/Rpc/PlayerCustomRpc.cs @@ -19,11 +19,10 @@ protected PlayerCustomRpc(TPlugin plugin, uint id) : base(plugin, id) /// Sends this custom rpc on the local player with the specified . /// /// The data to send. - /// Whether to send it immediately. /// The callback to invoke when this packet is acknowledged. - public void Send(TData data, bool immediately = false, Action? ackCallback = null) + public void Send(TData data, Action? ackCallback = null) { - Send(PlayerControl.LocalPlayer, data, immediately, ackCallback); + Send(PlayerControl.LocalPlayer, data, ackCallback); } /// @@ -52,11 +51,10 @@ protected PlayerCustomRpc(TPlugin plugin, uint id) : base(plugin, id) /// /// Sends this custom rpc on the local player. /// - /// Whether to send it immediately. /// The callback to invoke when this packet is acknowledged. - public void Send(bool immediately = false, Action? ackCallback = null) + public void Send(Action? ackCallback = null) { - Send(PlayerControl.LocalPlayer, immediately, ackCallback); + Send(PlayerControl.LocalPlayer, ackCallback); } /// diff --git a/Reactor/Networking/Rpc/UnsafeCustomRpc.cs b/Reactor/Networking/Rpc/UnsafeCustomRpc.cs index 67fefb40..4a37021f 100644 --- a/Reactor/Networking/Rpc/UnsafeCustomRpc.cs +++ b/Reactor/Networking/Rpc/UnsafeCustomRpc.cs @@ -91,10 +91,9 @@ protected UnsafeCustomRpc(BasePlugin plugin, uint id) /// /// The to send the rpc on. /// The data to send. - /// Whether to send it immediately. /// Target client id, defaults to broadcast. /// The callback to invoke when this packet is acknowledged. - public void UnsafeSend(InnerNetObject innerNetObject, object? data, bool immediately = false, int targetClientId = -1, Action? ackCallback = null) + public void UnsafeSend(InnerNetObject innerNetObject, object? data, int targetClientId = -1, Action? ackCallback = null) { ArgumentNullException.ThrowIfNull(innerNetObject); @@ -108,11 +107,6 @@ public void UnsafeSend(InnerNetObject innerNetObject, object? data, bool immedia UnsafeHandle(innerNetObject, data); } - if (immediately == false) - { - Warning("Non-immediate RPCs were removed in 2025.5.20! Reactor will now always send immediately!"); - } - var writer = AmongUsClient.Instance.StartRpcImmediately( innerNetObject.NetId, CustomRpcManager.CallId, diff --git a/Reactor/Networking/Serialization/INetSerializable.cs b/Reactor/Networking/Serialization/INetSerializable.cs new file mode 100644 index 00000000..768940c8 --- /dev/null +++ b/Reactor/Networking/Serialization/INetSerializable.cs @@ -0,0 +1,21 @@ +using Hazel; + +namespace Reactor.Networking.Serialization; + +/// +/// Provides a read-write method pair so that data types can define how to read and write themselves. +/// +public interface INetSerializable +{ + /// + /// Writes the instance to the provided writer. + /// + /// The to write to. + void WriteTo(MessageWriter writer); + + /// + /// Reads the instance's properties from the provided reader. + /// + /// The to read from. + void ReadFrom(MessageReader reader); +} diff --git a/Reactor/Networking/Serialization/MessageSerializer.cs b/Reactor/Networking/Serialization/MessageSerializer.cs index 5a1236cd..6f0e1cfb 100644 --- a/Reactor/Networking/Serialization/MessageSerializer.cs +++ b/Reactor/Networking/Serialization/MessageSerializer.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using Hazel; using Reactor.Networking.Extensions; using UnityEngine; @@ -12,10 +15,16 @@ namespace Reactor.Networking.Serialization; /// public static class MessageSerializer { + private static Dictionary MessageConverterMap { get; } = new(); private static List MessageConverters { get; } = new(); + private static List GenericConverters { get; } = new(); - private static Dictionary MessageConverterMap { get; } = new(); - private static Dictionary GenericConvertersMap { get; } = new(); + private enum SearchMode + { + Basic, + Generic, + Combined, + } internal static void ClearMaps() { @@ -30,68 +39,197 @@ public static void Register(Type type) { if (type.IsGenericTypeDefinition) { - var baseType = type.BaseType!; + var baseType = type.BaseType; + var isUnsafeConverter = baseType == typeof(UnsafeMessageConverter); + var isMessageConverterT = baseType != null && baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(MessageConverter<>); - if (!baseType.Name.Contains("MessageConverter")) - throw new InvalidOperationException($"{type.Name} should directly inherit from MessageConverter"); + if (!isMessageConverterT && !isUnsafeConverter) + throw new InvalidOperationException($"{type.Name} must directly inherit from either MessageConverter or UnsafeMessageConverter."); - var generics = baseType.GetGenericArguments(); - var param = generics[0]; - - GenericConvertersMap.Add(param.GetGenericTypeDefinition(), type); + GenericConverters.Add(type); } else { - var messageConverter = (UnsafeMessageConverter) Activator.CreateInstance(type)!; + var messageConverter = (UnsafeMessageConverter) CreateObject(type)!; MessageConverters.Add(messageConverter); } } /// - /// Finds a MessageConverter for the specified . + /// Finds a MessageConverter for the specified by checking generic converters first, then basic converters. /// - /// The type of an object. + /// The type of the object. /// A MessageConverter that can convert the specified . public static UnsafeMessageConverter? FindConverter(Type type) - { - if (!MessageConverterMap.TryGetValue(type, out var value)) - { - value = MessageConverters.SingleOrDefault(x => x.CanConvert(type)); - - if (value == null) - return null; - - MessageConverterMap.Add(type, value); - } + => ResolveAndCache(type, SearchMode.Combined); - return value; - } + /// + /// Finds a MessageConverter for the specified using basic converters. + /// + /// The type of the object. + /// A MessageConverter that can convert the specified . + public static UnsafeMessageConverter? FindBasicConverter(Type type) + => ResolveAndCache(type, SearchMode.Basic); /// /// Finds and builds a MessageConverter for the specified using a registered generic converter. /// - /// The type of an object. + /// The type of the object. /// A MessageConverter that can convert the specified . public static UnsafeMessageConverter? FindGenericConverter(Type type) + => ResolveAndCache(type, SearchMode.Generic); + + private static UnsafeMessageConverter? ResolveAndCache(Type type, SearchMode mode) { if (MessageConverterMap.TryGetValue(type, out var value)) return value; - if (!type.IsGenericType) + value = mode switch + { + SearchMode.Basic => FindBasicConverterInternal(type), + SearchMode.Generic => FindGenericConverterInternal(type), + SearchMode.Combined => FindBasicConverterInternal(type) ?? FindGenericConverterInternal(type), + _ => null, + }; + + MessageConverterMap[type] = value; + + return value; + } + + private static UnsafeMessageConverter? FindBasicConverterInternal(Type type) + { + return MessageConverters.SingleOrDefault(x => x.CanConvert(type)); + } + + private static UnsafeMessageConverter? FindGenericConverterInternal(Type type) + { + foreach (var converterTypeDef in GenericConverters) + { + var mappedArgs = MapGenericArguments(converterTypeDef, type); + + if (mappedArgs == null) + continue; + + var converterParams = converterTypeDef.GetGenericArguments(); + var allSatisfy = true; + + for (var i = 0; i < mappedArgs.Length; i++) + { + if (SatisfiesConstraints(mappedArgs[i], converterParams[i])) + continue; + + allSatisfy = false; + break; + } + + if (!allSatisfy) + continue; + + var genericConverterType = converterTypeDef.MakeGenericType(mappedArgs); + var converterInstance = (UnsafeMessageConverter) CreateObject(genericConverterType)!; + + MessageConverters.Add(converterInstance); + + return converterInstance; + } + + return null; + } + + private static Type[]? MapGenericArguments(Type converterTypeDef, Type targetType) + { + var baseType = converterTypeDef.BaseType; + + if (baseType == null) return null; - var typeDef = type.GetGenericTypeDefinition(); + if (baseType == typeof(UnsafeMessageConverter)) + { + var converterParams = converterTypeDef.GetGenericArguments(); + + if (converterParams.Length == 1) + return new[] { targetType }; + + if (targetType.IsGenericType && converterParams.Length == targetType.GetGenericArguments().Length) + return targetType.GetGenericArguments(); - if (!GenericConvertersMap.TryGetValue(typeDef, out var builder)) return null; + } - var generic = builder.MakeGenericType(type.GetGenericArguments()); - value = (UnsafeMessageConverter) Activator.CreateInstance(generic)!; + if (!baseType.IsGenericType) + return null; - MessageConverters.Add(value); - MessageConverterMap.Add(type, value); + var templateType = baseType.GetGenericArguments()[0]; - return value; + if (templateType.IsGenericParameter) + return new[] { targetType }; + + if (!templateType.IsGenericType || !targetType.IsGenericType) + { + return null; + } + + var templateArgs = templateType.GetGenericArguments(); + var targetArgs = targetType.GetGenericArguments(); + + if (templateArgs.Length != targetArgs.Length) + return null; + + var mappedArgs = new Type[converterTypeDef.GetGenericArguments().Length]; + + for (var i = 0; i < templateArgs.Length; i++) + { + var templateArg = templateArgs[i]; + var targetArg = targetArgs[i]; + + if (templateArg.IsGenericParameter) + mappedArgs[templateArg.GenericParameterPosition] = targetArg; + else if (templateArg != targetArg) + return null; + } + + return mappedArgs.Any(t => t == null) ? null : mappedArgs; + } + + private static bool SatisfiesConstraints(Type targetType, Type genericParam) + { + var attributes = genericParam.GenericParameterAttributes; + var constraints = genericParam.GetGenericParameterConstraints(); + + // struct constraint + if (attributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) && !targetType.IsValueType) + return false; + + // class constraint + if (attributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) && targetType.IsValueType) + return false; + + // new() constraint + if (attributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) && !targetType.IsValueType && targetType.GetConstructor(Type.EmptyTypes) == null) + return false; + + // notnull constraint + if (genericParam.CustomAttributes.Any(a => a.AttributeType.Name == "NullableAttribute")) + { + if (Nullable.GetUnderlyingType(targetType) != null) + return false; + + // Nullable reference types cannot be distinguished, so we let them through here + } + + // Explicit type constraints + foreach (var constraint in constraints) + { + var check = constraint == typeof(Enum) + ? !targetType.IsEnum + : !constraint.IsAssignableFrom(targetType); + + if (check) + return false; + } + + return true; } /// @@ -138,10 +276,10 @@ public static void Serialize(this MessageWriter writer, object @object) writer.Write(i); break; case ulong i: - writer.Write(i); + writer.WritePacked(i); break; case long i: - ExtraMessageExtensions.Write(writer, i); // For some reason this insists on referring the to float write method, so this is me taking precautions + writer.WritePacked(i); break; case Vector2 i: writer.Write(i); @@ -158,9 +296,12 @@ public static void Serialize(this MessageWriter writer, object @object) case Enum i: writer.Write(i); break; + case INetSerializable i: + i.WriteTo(writer); + break; default: var type = @object.GetType(); - var converter = FindGenericConverter(type) ?? FindConverter(type); + var converter = FindConverter(type); if (converter != null) converter.UnsafeWrite(writer, @object); @@ -179,6 +320,24 @@ public static void Serialize(this MessageWriter writer, object @object) /// A generic value from the . public static T Deserialize(this MessageReader reader) => (T) reader.Deserialize(typeof(T)); + private static readonly Dictionary> _basicReaders = new() + { + [typeof(int)] = reader => reader.ReadPackedInt32(), + [typeof(uint)] = reader => reader.ReadPackedUInt32(), + [typeof(byte)] = reader => reader.ReadByte(), + [typeof(float)] = reader => reader.ReadSingle(), + [typeof(sbyte)] = reader => reader.ReadSByte(), + [typeof(ushort)] = reader => reader.ReadUInt16(), + [typeof(short)] = reader => reader.ReadInt16(), + [typeof(bool)] = reader => reader.ReadBoolean(), + [typeof(Vector2)] = reader => reader.ReadVector2(), + [typeof(string)] = reader => reader.ReadString(), + [typeof(ulong)] = reader => reader.ReadPackedUInt64(), + [typeof(long)] = reader => reader.ReadPackedInt64(), + [typeof(Color)] = reader => reader.ReadColor(), + [typeof(Color32)] = reader => reader.ReadColor32(), + }; + /// /// Deserializes an of from the . /// @@ -187,83 +346,45 @@ public static void Serialize(this MessageWriter writer, object @object) /// An from the . public static object Deserialize(this MessageReader reader, Type objectType) { - if (objectType == typeof(int)) - { - return reader.ReadPackedInt32(); - } - - if (objectType == typeof(uint)) + if (_basicReaders.TryGetValue(objectType, out var readDel)) { - return reader.ReadPackedUInt32(); + return readDel(reader); } - if (objectType == typeof(byte)) - { - return reader.ReadByte(); - } - - if (objectType == typeof(float)) - { - return reader.ReadSingle(); - } - - if (objectType == typeof(sbyte)) - { - return reader.ReadSByte(); - } - - if (objectType == typeof(ushort)) - { - return reader.ReadUInt16(); - } - - if (objectType == typeof(bool)) - { - return reader.ReadBoolean(); - } - - if (objectType == typeof(Vector2)) - { - return reader.ReadVector2(); - } - - if (objectType == typeof(string)) + if (objectType.IsEnum) { - return reader.ReadString(); + return reader.ReadEnum(objectType); } - if (objectType == typeof(ulong)) + if (objectType.IsAssignableTo(typeof(INetSerializable))) { - return reader.ReadUInt64(); + var instance = (INetSerializable) CreateObject(objectType); + instance.ReadFrom(reader); + return instance; } - if (objectType == typeof(long)) - { - return reader.ReadInt64(); - } + var converter = FindConverter(objectType); - if (objectType == typeof(Color)) + if (converter != null) { - return reader.ReadColor(); + return converter.UnsafeRead(reader, objectType); } - if (objectType == typeof(Color32)) - { - return reader.ReadColor32(); - } + throw new NotSupportedException("Couldn't deserialize " + objectType); + } - if (objectType.IsEnum) - { - return reader.ReadEnum(objectType); - } + private static readonly ConcurrentDictionary _constructors = new(); - var converter = FindGenericConverter(objectType) ?? FindConverter(objectType); + private static object CreateObject(Type objectType) + { + var construct = _constructors.GetOrAdd(objectType, GetConstruct); - if (converter != null) - { - return converter.UnsafeRead(reader, objectType); - } + if (construct != null) + return construct.Invoke(null); - throw new NotSupportedException("Couldn't deserialize " + objectType); + Warning($"{objectType.Name} does not have a parameterless constructor. It is highly recommended that one is added so that field initialisers are not skipped."); + return RuntimeHelpers.GetUninitializedObject(objectType); } + + private static ConstructorInfo? GetConstruct(Type type) => type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes); }