diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index bceab67bb..2c6a3f155 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -68,6 +68,12 @@ inline uint32 GetHash(const NetworkReplicatedObject& key) return GetHash(key.ObjectId); } +struct Serializer +{ + NetworkReplicator::SerializeFunc Methods[2]; + void* Tags[2]; +}; + namespace { CriticalSection ObjectsLock; @@ -76,6 +82,7 @@ namespace NetworkStream* CachedWriteStream = nullptr; NetworkStream* CachedReadStream = nullptr; Array CachedTargets; + Dictionary SerializersTable; } class NetworkReplicationService : public EngineService @@ -96,14 +103,12 @@ void NetworkReplicationService::Dispose() NetworkReplicationService NetworkReplicationServiceInstance; -Dictionary NetworkReplicator::SerializersTable; - -void INetworkSerializable_Serialize(void* instance, NetworkStream* stream) +void INetworkSerializable_Serialize(void* instance, NetworkStream* stream, void* tag) { ((INetworkSerializable*)instance)->Serialize(stream); } -void INetworkSerializable_Deserialize(void* instance, NetworkStream* stream) +void INetworkSerializable_Deserialize(void* instance, NetworkStream* stream, void* tag) { ((INetworkSerializable*)instance)->Deserialize(stream); } @@ -143,23 +148,60 @@ NetworkReplicatedObject* ResolveObject(Guid objectId, Guid ownerId, char objectT return nullptr; } -NetworkReplicator::SerializeFuncPair NetworkReplicator::GetSerializer(const ScriptingTypeHandle& typeHandle) +#if !COMPILE_WITHOUT_CSHARP + +#include "Engine/Scripting/ManagedCLR/MUtils.h" + +void INetworkSerializable_Managed(void* instance, NetworkStream* stream, void* tag) { + auto signature = (Function::Signature)tag; + signature(instance, stream); +} + +void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, const Function& serialize, const Function& deserialize) +{ + if (!typeHandle) + return; + + // This assumes that C# glue code passed static method pointer (via Marshal.GetFunctionPointerForDelegate) + const Serializer serializer{ INetworkSerializable_Managed, INetworkSerializable_Managed, *(SerializeFunc*)&serialize, *(SerializeFunc*)&deserialize }; + SerializersTable.Add(typeHandle, serializer); +} + +#endif + +void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, SerializeFunc serialize, SerializeFunc deserialize, void* serializeTag, void* deserializeTag) +{ + const Serializer serializer{ serialize, deserialize, serializeTag, deserializeTag }; + SerializersTable.Add(typeHandle, serializer); +} + +bool NetworkReplicator::InvokeSerializer(const ScriptingTypeHandle& typeHandle, void* instance, NetworkStream* stream, bool serialize) +{ + if (!typeHandle || !instance || !stream) + return true; + // Get serializers pair from table - SerializeFuncPair result(nullptr, nullptr); - if (!SerializersTable.TryGet(typeHandle, result)) + Serializer serializer; + if (!SerializersTable.TryGet(typeHandle, serializer)) { // Fallback to INetworkSerializable interface (if type implements it) const ScriptingType& type = typeHandle.GetType(); const ScriptingType::InterfaceImplementation* interface = type.GetInterface(INetworkSerializable::TypeInitializer); if (interface) { - result.First = INetworkSerializable_Serialize; - result.Second = INetworkSerializable_Deserialize; - SerializersTable.Add(typeHandle, result); + serializer.Methods[0] = INetworkSerializable_Serialize; + serializer.Methods[1] = INetworkSerializable_Deserialize; + SerializersTable.Add(typeHandle, serializer); } + else + return true; } - return result; + + // Invoke serializer + const byte idx = serialize ? 0 : 1; + serializer.Methods[idx](instance, stream, serializer.Tags[idx]); + return false; } void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* owner) @@ -247,12 +289,8 @@ void NetworkInternal::NetworkReplicatorUpdate() // Serialize object stream->Initialize(); - const auto serializerFunc = NetworkReplicator::GetSerializer(obj->GetTypeHandle()).First; - if (serializerFunc) - { - serializerFunc(obj, stream); - } - else + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); + if (failed) { #if NETWORK_REPLICATOR_DEBUG_LOG if (!item.InvalidTypeWarn) @@ -310,15 +348,11 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.DataSize); // Deserialize object - const auto deserializerFunc = NetworkReplicator::GetSerializer(obj->GetTypeHandle()).Second; - if (deserializerFunc) - { - deserializerFunc(obj, stream); - } - else + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, false); + if (failed) { #if NETWORK_REPLICATOR_DEBUG_LOG - if (!item.InvalidTypeWarn) + if (failed && !item.InvalidTypeWarn) { item.InvalidTypeWarn = true; LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); diff --git a/Source/Engine/Networking/NetworkReplicator.cs b/Source/Engine/Networking/NetworkReplicator.cs new file mode 100644 index 000000000..6c746e7b9 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicator.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +namespace FlaxEngine.Networking +{ + partial class NetworkReplicator + { + private static Dictionary> _managedSerializers; + +#if FLAX_EDITOR + private static void OnScriptsReloadBegin() + { + // Clear refs to managed types that will be hot-reloaded + _managedSerializers.Clear(); + _managedSerializers = null; + FlaxEditor.ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + } +#endif + + /// + /// Network object replication serialization/deserialization delegate. + /// + /// + /// Use Object.FromUnmanagedPtr(instancePtr/streamPtr) to get object or NetworkStream from raw native pointers. + /// + /// var instance = Object.FromUnmanagedPtr(instancePtr) + /// var stream = (NetworkStream)Object.FromUnmanagedPtr(streamPtr) + public delegate void SerializeFunc(IntPtr instancePtr, IntPtr streamPtr); + + /// + /// Registers a new serialization methods for a given C# type. + /// + /// + /// Use Object.FromUnmanagedPtr(instancePtr/streamPtr) to get object or NetworkStream from raw native pointers. + /// + /// The C# type (class or structure). + /// Function to call for value serialization. + /// Function to call for value deserialization. + [Unmanaged] + public static void AddSerializer(Type type, SerializeFunc serialize, SerializeFunc deserialize) + { + // C#-only types (eg. custom C# structures) cannot use native serializers due to missing ScriptingType + if (typeof(FlaxEngine.Object).IsAssignableFrom(type)) + { + Internal_AddSerializer(type, Marshal.GetFunctionPointerForDelegate(serialize), Marshal.GetFunctionPointerForDelegate(deserialize)); + } + else + { + if (_managedSerializers == null) + { + _managedSerializers = new Dictionary>(); +#if FLAX_EDITOR + FlaxEditor.ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; +#endif + } + _managedSerializers[type] = new KeyValuePair(serialize, deserialize); + } + } + + /// + /// Invokes the network replication serializer for a given type. + /// + /// The scripting type to serialize. + /// The value instance to serialize. + /// The input/output stream to use for serialization. + /// True if serialize, otherwise deserialize mode. + /// True if failed, otherwise false. + [Unmanaged] + public static bool InvokeSerializer(Type type, FlaxEngine.Object instance, NetworkStream stream, bool serialize) + { + return Internal_InvokeSerializer(type, FlaxEngine.Object.GetUnmanagedPtr(instance), FlaxEngine.Object.GetUnmanagedPtr(stream), serialize); + } + + /// + /// Invokes the network replication serializer for a given type. + /// + /// The scripting type to serialize. + /// The value instance to serialize. + /// The input/output stream to use for serialization. + /// True if serialize, otherwise deserialize mode. + /// True if failed, otherwise false. + [Unmanaged] + public static bool InvokeSerializer(System.Type type, IntPtr instance, NetworkStream stream, bool serialize) + { + if (_managedSerializers != null && _managedSerializers.TryGetValue(type, out var e)) + { + var serializer = serialize ? e.Key : e.Value; + serializer(instance, FlaxEngine.Object.GetUnmanagedPtr(stream)); + return false; + } + return Internal_InvokeSerializer(type, instance, FlaxEngine.Object.GetUnmanagedPtr(stream), serialize); + } + } +} diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 90acc0bc0..14a40070a 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -3,8 +3,6 @@ #pragma once #include "Types.h" -#include "Engine/Core/Types/Pair.h" -#include "Engine/Core/Collections/Dictionary.h" #include "Engine/Scripting/ScriptingObject.h" #include "Engine/Scripting/ScriptingType.h" @@ -14,18 +12,30 @@ API_CLASS(static, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicator { DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicator); + friend class NetworkReplicatorInternal; + typedef void (*SerializeFunc)(void* instance, NetworkStream* stream, void* tag); public: - typedef void (*SerializeFunc)(void* instance, NetworkStream* stream); - typedef Pair SerializeFuncPair; - static SerializeFuncPair GetSerializer(const ScriptingTypeHandle& typeHandle); + /// + /// Adds the network replication serializer for a given type. + /// + /// The scripting type to serialize. + /// Serialization callback method. + /// Deserialization callback method. + /// Serialization callback method tag value. + /// Deserialization callback method tag value. + static void AddSerializer(const ScriptingTypeHandle& typeHandle, SerializeFunc serialize, SerializeFunc deserialize, void* serializeTag = nullptr, void* deserializeTag = nullptr); /// - /// Global table for registered types serialization methods (key is type name, value is pair of methods to serialize and deserialize object). + /// Invokes the network replication serializer for a given type. /// - static Dictionary SerializersTable; + /// The scripting type to serialize. + /// The value instance to serialize. + /// The input/output stream to use for serialization. + /// True if serialize, otherwise deserialize mode. + /// True if failed, otherwise false. + API_FUNCTION(NoProxy) static bool InvokeSerializer(const ScriptingTypeHandle& typeHandle, void* instance, NetworkStream* stream, bool serialize); -public: /// /// Adds the object to the network replication system. /// @@ -33,4 +43,9 @@ public: /// The object to replicate. /// The owner of the object (eg. player that spawned it). API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* owner); + +private: +#if !COMPILE_WITHOUT_CSHARP + API_FUNCTION(NoProxy) static void AddSerializer(const ScriptingTypeHandle& type, const Function& serialize, const Function& deserialize); +#endif }; diff --git a/Source/Engine/Scripting/Attributes/NetworkReplicatedAttribute.cs b/Source/Engine/Scripting/Attributes/NetworkReplicatedAttribute.cs new file mode 100644 index 000000000..d29a63df3 --- /dev/null +++ b/Source/Engine/Scripting/Attributes/NetworkReplicatedAttribute.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine +{ + /// + /// Indicates that a field or a property should be replicated over network. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class NetworkReplicatedAttribute : Attribute + { + } +} diff --git a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs index e324aaff0..5fd2341d7 100644 --- a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs +++ b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs @@ -2,8 +2,13 @@ using System; using System.Text; +using System.Linq; +using System.IO; using System.Collections.Generic; +using Flax.Build.Graph; using Flax.Build.Bindings; +using Mono.Cecil; +using Mono.Cecil.Cil; namespace Flax.Build.Plugins { @@ -13,7 +18,48 @@ namespace Flax.Build.Plugins /// internal sealed class NetworkingPlugin : Plugin { + private struct InBuildSerializer + { + public string WriteMethod; + public string ReadMethod; + + public InBuildSerializer(string write, string read) + { + WriteMethod = write; + ReadMethod = read; + } + } + + private struct TypeSerializer + { + public TypeDefinition Type; + public MethodDefinition Serialize; + public MethodDefinition Deserialize; + } + internal const string NetworkReplicated = "NetworkReplicated"; + private const string Thunk1 = "INetworkSerializable_Serialize"; + private const string Thunk2 = "INetworkSerializable_Deserialize"; + private static Dictionary _inBuildSerializers = new Dictionary() + { + { "System.Boolean", new InBuildSerializer("WriteBoolean", "ReadBoolean") }, + { "System.Single", new InBuildSerializer("WriteSingle", "ReadSingle") }, + { "System.Double", new InBuildSerializer("WriteDouble", "ReadDouble") }, + { "System.Int64", new InBuildSerializer("WriteInt64", "ReadInt64") }, + { "System.Int32", new InBuildSerializer("WriteInt32", "ReadInt32") }, + { "System.Int16", new InBuildSerializer("WriteInt16", "ReadInt16") }, + { "System.SByte", new InBuildSerializer("WriteSByte", "ReadSByte") }, + { "System.UInt64", new InBuildSerializer("WriteUInt64", "ReadUInt64") }, + { "System.UInt32", new InBuildSerializer("WriteUInt32", "ReadUInt32") }, + { "System.UInt16", new InBuildSerializer("WriteUInt16", "ReadUInt16") }, + { "System.Byte", new InBuildSerializer("WriteByte", "ReadByte") }, + { "System.String", new InBuildSerializer("WriteString", "ReadString") }, + { "System.Guid", new InBuildSerializer("WriteGuid", "ReadGuid") }, + { "FlaxEngine.Vector2", new InBuildSerializer("WriteVector2", "ReadVector2") }, + { "FlaxEngine.Vector3", new InBuildSerializer("WriteVector3", "ReadVector3") }, + { "FlaxEngine.Vector4", new InBuildSerializer("WriteVector4", "ReadVector4") }, + { "FlaxEngine.Quaternion", new InBuildSerializer("WriteQuaternion", "ReadQuaternion") }, + }; /// public override void Init() @@ -23,6 +69,8 @@ namespace Flax.Build.Plugins BindingsGenerator.ParseMemberTag += OnParseMemberTag; BindingsGenerator.GenerateCppTypeInternals += OnGenerateCppTypeInternals; BindingsGenerator.GenerateCppTypeInitRuntime += OnGenerateCppTypeInitRuntime; + BindingsGenerator.GenerateCSharpTypeInternals += OnGenerateCSharpTypeInternals; + Builder.BuildDotNetAssembly += OnBuildDotNetAssembly; } private void OnParseMemberTag(ref bool valid, BindingsGenerator.TagParameter tag, MemberInfo memberInfo) @@ -81,22 +129,20 @@ namespace Flax.Build.Plugins private void OnGenerateCppTypeSerialize(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents, List fields, bool serialize) { - var thunk1 = "INetworkSerializable_Serialize"; - var thunk2 = "INetworkSerializable_Deserialize"; - contents.Append(" static void ").Append(serialize ? thunk1 : thunk2).AppendLine("(void* instance, NetworkStream* stream)"); + contents.Append(" static void ").Append(serialize ? Thunk1 : Thunk2).AppendLine("(void* instance, NetworkStream* stream, void* tag)"); contents.AppendLine(" {"); - contents.AppendLine($" {typeInfo.NativeName}& object = *({typeInfo.NativeName}*)instance;"); + contents.AppendLine($" {typeInfo.NativeName}& obj = *({typeInfo.NativeName}*)instance;"); if (IsRawPOD(buildData, typeInfo)) { // POD types as raw bytes - OnGenerateCppWriteRaw(contents, "object", serialize); + OnGenerateCppWriteRaw(contents, "&obj", serialize); } else { if (typeInfo is ClassStructInfo classStructInfo && classStructInfo.BaseType != null && classStructInfo.BaseType.Name != "ScriptingObject") { // Replicate base type - OnGenerateCppWriteSerializer(contents, classStructInfo.BaseType.NativeName, "object", serialize); + OnGenerateCppWriteSerializer(contents, classStructInfo.BaseType.NativeName, "obj", serialize); } // Replicate all marked fields @@ -106,7 +152,7 @@ namespace Flax.Build.Plugins { if (fieldInfo.GetTag(NetworkReplicated) == null) continue; - OnGenerateCppTypeSerializeData(buildData, typeInfo, contents, fieldInfo.Type, $"object.{fieldInfo.Name}", serialize); + OnGenerateCppTypeSerializeData(buildData, typeInfo, contents, fieldInfo.Type, $"obj.{fieldInfo.Name}", serialize); } } // TODO: add support for class properties replication @@ -185,10 +231,8 @@ namespace Flax.Build.Plugins private void OnGenerateCppWriteSerializer(StringBuilder contents, string type, string data, bool serialize) { - var comp = serialize ? "First" : "Second"; - contents.AppendLine($" {{const auto serializer = NetworkReplicator::GetSerializer({type}::TypeInitializer);"); - contents.AppendLine($" if (serializer.{comp})"); - contents.AppendLine($" serializer.{comp}(&{data}, stream);}}"); + var mode = serialize ? "true" : "false"; + contents.AppendLine($" NetworkReplicator::InvokeSerializer({type}::TypeInitializer, &{data}, stream, {mode});"); } private void OnGenerateCppTypeInitRuntime(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents) @@ -199,7 +243,569 @@ namespace Flax.Build.Plugins var typeNameInternal = typeInfo.FullNameNativeInternal; // Register generated serializer functions - contents.AppendLine($" NetworkReplicator::SerializersTable[ScriptingTypeHandle({typeNameNative}::TypeInitializer)] = NetworkReplicator::SerializeFuncPair({typeNameInternal}Internal::INetworkSerializable_Serialize, {typeNameInternal}Internal::INetworkSerializable_Deserialize);"); + contents.AppendLine($" NetworkReplicator::AddSerializer(ScriptingTypeHandle({typeNameNative}::TypeInitializer), {typeNameInternal}Internal::INetworkSerializable_Serialize, {typeNameInternal}Internal::INetworkSerializable_Deserialize);"); + } + + private void OnGenerateCSharpTypeInternals(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents, string indent) + { + // Skip types that don't use networking + if (typeInfo.GetTag(NetworkReplicated) == null) + return; + + if (typeInfo is ClassInfo) + return; + + // Generate C# wrapper functions to serialize/deserialize type directly from managed code + OnGenerateCSharpTypeSerialize(buildData, typeInfo, contents, indent, true); + OnGenerateCSharpTypeSerialize(buildData, typeInfo, contents, indent, false); + + } + + private void OnGenerateCSharpTypeSerialize(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents, string indent, bool serialize) + { + var mode = serialize ? "true" : "false"; + contents.AppendLine(); + contents.Append(indent).Append("public void ").Append(serialize ? Thunk1 : Thunk2).AppendLine("(FlaxEngine.Networking.NetworkStream stream)"); + contents.Append(indent).AppendLine("{"); + if (typeInfo is ClassInfo) + { + contents.Append(indent).AppendLine($" FlaxEngine.Networking.NetworkReplicator.InvokeSerializer(typeof({typeInfo.Name}), this, stream, {mode});"); + } + else + { + if (typeInfo.IsPod) + { + contents.Append(indent).AppendLine($" fixed ({typeInfo.Name}* ptr = &this)"); + contents.Append(indent).AppendLine($" FlaxEngine.Networking.NetworkReplicator.InvokeSerializer(typeof({typeInfo.Name}), new IntPtr(ptr), stream, {mode});"); + } + else + { + // TODO: generate C# data serialization (eg. in OnPatchAssembly) or generate internal call with managed -> native data conversion + contents.Append(indent).AppendLine($" throw new NotImplementedException(\"Not supported native structure with references used in managed code for replication.\");"); + } + } + contents.Append(indent).AppendLine("}"); + } + + private void OnBuildDotNetAssembly(TaskGraph graph, Builder.BuildData buildData, NativeCpp.BuildOptions buildOptions, Task buildTask, IGrouping binaryModule) + { + // Skip assemblies not using netowrking + if (!binaryModule.Any(module => module.Tags.ContainsKey(NetworkReplicated))) + return; + + // Generate netoworking code inside assembly after it's being compiled + var assemblyPath = buildTask.ProducedFiles[0]; + var task = graph.Add(); + task.ProducedFiles.Add(assemblyPath); + task.WorkingDirectory = buildTask.WorkingDirectory; + task.Command = () => OnPatchAssembly(buildData, buildOptions, buildTask, assemblyPath); + task.CommandPath = null; + task.InfoMessage = $"Generating netowrking code for {Path.GetFileName(assemblyPath)}..."; + task.Cost = 50; + task.DisableCache = true; + task.DependentTasks = new HashSet(); + task.DependentTasks.Add(buildTask); + } + + private void OnPatchAssembly(Builder.BuildData buildData, NativeCpp.BuildOptions buildOptions, Task buildTask, string assemblyPath) + { + using (DefaultAssemblyResolver assemblyResolver = new DefaultAssemblyResolver()) + using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, new ReaderParameters{ ReadWrite = true, ReadSymbols = true, AssemblyResolver = assemblyResolver })) + { + // Setup module search locations + var searchDirectories = new HashSet(); + searchDirectories.Add(Path.GetDirectoryName(assemblyPath)); + foreach (var file in buildTask.PrerequisiteFiles) + { + if (file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + searchDirectories.Add(Path.GetDirectoryName(file)); + } + foreach (var e in searchDirectories) + assemblyResolver.AddSearchDirectory(e); + + ModuleDefinition module = assembly.MainModule; + TypeReference voidType = module.ImportReference(typeof(void)); + module.GetType("FlaxEngine.Networking.NetworkStream", out var networkStreamType); + + // Process all types within a module + bool modified = false; + bool failed = false; + var addSerializers = new List(); + foreach (TypeDefinition type in module.Types) + { + if (type.IsInterface || type.IsEnum) + continue; + var isNative = type.HasAttribute("FlaxEngine.UnmanagedAttribute"); + if (isNative) + continue; + if (type.HasMethod(Thunk1) || type.HasMethod(Thunk2)) + continue; + var isINetworkSerializable = type.HasInterface("FlaxEngine.Networking.INetworkSerializable"); + MethodDefinition serializeINetworkSerializable = null, deserializeINetworkSerializable = null; + if (isINetworkSerializable) + { + foreach (MethodDefinition m in type.Methods) + { + if (m.HasBody && m.Parameters.Count == 1 && m.Parameters[0].ParameterType.FullName == "FlaxEngine.Networking.NetworkStream") + { + if (m.Name == "Serialize") + serializeINetworkSerializable = m; + else if (m.Name == "Deserialize") + deserializeINetworkSerializable = m; + } + } + } + var isNetworkReplicated = false; + foreach (FieldDefinition f in type.Fields) + { + if (!f.HasAttribute("FlaxEngine.NetworkReplicatedAttribute")) + continue; + isNetworkReplicated = true; + break; + } + + + if (type.IsValueType) + { + if (isINetworkSerializable) + { + // Generate INetworkSerializable interface method calls + GenerateCallINetworkSerializable(type, Thunk1, voidType, networkStreamType, serializeINetworkSerializable); + GenerateCallINetworkSerializable(type, Thunk2, voidType, networkStreamType, deserializeINetworkSerializable); + modified = true; + } + else if (isNetworkReplicated) + { + // Generate serializization methods + GenerateSerializer(type, true, ref failed, Thunk1, voidType, networkStreamType); + GenerateSerializer(type, false, ref failed, Thunk2, voidType, networkStreamType); + modified = true; + } + } + else if (!isINetworkSerializable && isNetworkReplicated) + { + // Generate serializization methods + var addSerializer = new TypeSerializer(); + addSerializer.Type = type; + addSerializer.Serialize = GenerateNativeSerializer(type, true, ref failed, Thunk1, voidType, networkStreamType); + addSerializer.Deserialize = GenerateNativeSerializer(type, false, ref failed, Thunk2, voidType, networkStreamType); + addSerializers.Add(addSerializer); + modified = true; + } + } + if (failed) + throw new Exception($"Failed to generate network replication for assembly {assemblyPath}"); + if (!modified) + return; + + // Generate serializers initializer (invoked on module load) + if (addSerializers.Count != 0) + { + // Create class + var name = "Initializer"; + var idx = 0; + while (module.Types.Any(x => x.Name == name)) + name = "Initializer" + idx++; + var c = new TypeDefinition("", name, TypeAttributes.Class | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit); + module.GetType("System.Object", out var objectType); + c.BaseType = module.ImportReference(objectType); + module.Types.Add(c); + + // Add ModuleInitializer attribute + module.GetType("FlaxEngine.ModuleInitializerAttribute", out var moduleInitializerAttribute); + var ctor1 = moduleInitializerAttribute.Resolve(); + MethodDefinition ctor = moduleInitializerAttribute.Resolve().Methods[0]; + var attribute = new CustomAttribute(module.ImportReference(ctor)); + c.CustomAttributes.Add(attribute); + + // Add Init method + var m = new MethodDefinition("Init", MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig, voidType); + ILProcessor il = m.Body.GetILProcessor(); + il.Emit(OpCodes.Nop); + module.GetType("System.Type", out var typeType); + var getTypeFromHandle = typeType.Resolve().GetMethod("GetTypeFromHandle"); + module.GetType("FlaxEngine.Networking.NetworkReplicator", out var networkReplicatorType); + var addSerializer = networkReplicatorType.Resolve().GetMethod("AddSerializer", 3); + module.ImportReference(addSerializer); + var serializeFuncType = addSerializer.Parameters[1].ParameterType; + var serializeFuncCtor = serializeFuncType.Resolve().GetMethod(".ctor"); + foreach (var e in addSerializers) + { + // NetworkReplicator.AddSerializer(typeof(), .INetworkSerializable_SerializeNative, .INetworkSerializable_DeserializeNative); + il.Emit(OpCodes.Ldtoken, e.Type); + il.Emit(OpCodes.Call, module.ImportReference(getTypeFromHandle)); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldftn, e.Serialize); + il.Emit(OpCodes.Newobj, module.ImportReference(serializeFuncCtor)); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldftn, e.Deserialize); + il.Emit(OpCodes.Newobj, module.ImportReference(serializeFuncCtor)); + il.Emit(OpCodes.Call, module.ImportReference(addSerializer)); + } + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ret); + c.Methods.Add(m); + } + + // Serialize assembly back to the file + assembly.Write(new WriterParameters { WriteSymbols = true } ); + } + } + + private static void GenerateCallINetworkSerializable(TypeDefinition type, string name, TypeReference voidType, TypeReference networkStreamType, MethodDefinition method) + { + var m = new MethodDefinition(name, MethodAttributes.Public | MethodAttributes.HideBySig, voidType); + m.Parameters.Add(new ParameterDefinition("stream", ParameterAttributes.None, networkStreamType)); + ILProcessor il = m.Body.GetILProcessor(); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, method); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ret); + type.Methods.Add(m); + } + + private static MethodDefinition GenerateSerializer(TypeDefinition type, bool serialize, ref bool failed, string name, TypeReference voidType, TypeReference networkStreamType) + { + ModuleDefinition module = type.Module; + var m = new MethodDefinition(name, MethodAttributes.Public | MethodAttributes.HideBySig, voidType); + m.Parameters.Add(new ParameterDefinition("stream", ParameterAttributes.None, networkStreamType)); + TypeDefinition networkStream = networkStreamType.Resolve(); + ILProcessor il = m.Body.GetILProcessor(); + il.Emit(OpCodes.Nop); + + // Serialize base type + if (type.BaseType != null && type.BaseType.FullName != "System.ValueType" && type.BaseType.FullName != "FlaxEngine.Object" && type.BaseType.CanBeResolved()) + { + GenerateSerializeCallback(module, il, type.BaseType.Resolve(), serialize); + } + + // Serialize all type fields marked with NetworkReplicated attribute + foreach (FieldDefinition f in type.Fields) + { + if (!f.HasAttribute("FlaxEngine.NetworkReplicatedAttribute")) + continue; + GenerateSerializerType(type, serialize, ref failed, f, f.FieldType, il, networkStream); + } + + if (serialize) + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ret); + type.Methods.Add(m); + return m; + } + + private static MethodDefinition GenerateNativeSerializer(TypeDefinition type, bool serialize, ref bool failed, string name, TypeReference voidType, TypeReference networkStreamType) + { + ModuleDefinition module = type.Module; + module.GetType("System.IntPtr", out var intPtrType); + module.GetType("FlaxEngine.Object", out var scriptingObjectType); + var fromUnmanagedPtr = scriptingObjectType.Resolve().GetMethod("FromUnmanagedPtr"); + + var m = new MethodDefinition(name + "Native", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, voidType); + m.Parameters.Add(new ParameterDefinition("instancePtr", ParameterAttributes.None, intPtrType)); + m.Parameters.Add(new ParameterDefinition("streamPtr", ParameterAttributes.None, intPtrType)); + TypeReference networkStream = module.ImportReference(networkStreamType); + ILProcessor il = m.Body.GetILProcessor(); + il.Emit(OpCodes.Nop); + il.Body.InitLocals = true; + + // instance = ()FlaxEngine.Object.FromUnmanagedPtr(instancePtr) + il.Body.Variables.Add(new VariableDefinition(type)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, module.ImportReference(fromUnmanagedPtr)); + il.Emit(OpCodes.Castclass, type); + il.Emit(OpCodes.Stloc_0); + + // NetworkStream stream = (NetworkStream)FlaxEngine.Object.FromUnmanagedPtr(streamPtr) + il.Body.Variables.Add(new VariableDefinition(networkStream)); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, module.ImportReference(fromUnmanagedPtr)); + il.Emit(OpCodes.Castclass, module.ImportReference(networkStream)); + il.Emit(OpCodes.Stloc_1); + + // Generate normal serializer + var serializer = GenerateSerializer(type, serialize, ref failed, name, voidType, networkStreamType); + + // Call serializer + il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ldloc_1); + il.Emit(OpCodes.Callvirt, serializer); + + if (serialize) + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ret); + type.Methods.Add(m); + return m; + } + + private static void GenerateSerializeCallback(ModuleDefinition module, ILProcessor il, TypeDefinition type, bool serialize) + { + if (type.IsScriptingObject()) + { + // NetworkReplicator.InvokeSerializer(typeof(), instance, stream, ) + il.Emit(OpCodes.Ldtoken, module.ImportReference(type)); + module.GetType("System.Type", out var typeType); + var getTypeFromHandle = typeType.Resolve().GetMethod("GetTypeFromHandle"); + il.Emit(OpCodes.Call, module.ImportReference(getTypeFromHandle)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + if (serialize) + il.Emit(OpCodes.Ldc_I4_1); + else + il.Emit(OpCodes.Ldc_I4_0); + module.GetType("FlaxEngine.Networking.NetworkReplicator", out var networkReplicatorType); + var invokeSerializer = networkReplicatorType.Resolve().GetMethod("InvokeSerializer", 4); + il.Emit(OpCodes.Call, module.ImportReference(invokeSerializer)); + il.Emit(OpCodes.Pop); + } + else + { + throw new Exception($"Not supported base type for network serializer '{type.FullName}'"); + } + } + + private static void GenerateSerializerType(TypeDefinition type, bool serialize, ref bool failed, FieldReference field, TypeReference valueType, ILProcessor il, TypeDefinition networkStreamType) + { + ModuleDefinition module = type.Module; + TypeDefinition valueTypeDef = valueType.Resolve(); + if (_inBuildSerializers.TryGetValue(valueType.FullName, out var serializer)) + { + // Call NetworkStream method to write/read data + MethodDefinition m; + if (serialize) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + m = networkStreamType.GetMethod(serializer.WriteMethod); + } + else + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + m = networkStreamType.GetMethod(serializer.ReadMethod); + } + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + if (!serialize) + il.Emit(OpCodes.Stfld, field); + } + else if (valueType.IsScriptingObject()) + { + // Replicate ScriptingObject as Guid ID + module.GetType("System.Guid", out var guidType); + module.GetType("FlaxEngine.Object", out var scriptingObjectType); + if (serialize) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Dup); + Instruction jmp1 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp1); + il.Emit(OpCodes.Pop); + var guidEmpty = guidType.Resolve().GetField("Empty"); + il.Emit(OpCodes.Ldsfld, module.ImportReference(guidEmpty)); + Instruction jmp2 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp2); + il.Append(jmp1); + var getID = scriptingObjectType.Resolve().GetMethod("get_ID"); + il.Emit(OpCodes.Call, module.ImportReference(getID)); + il.Append(jmp2); + var m = networkStreamType.GetMethod("WriteGuid"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + } + else + { + var m = networkStreamType.GetMethod("ReadGuid"); + module.GetType("System.Type", out var typeType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Ldarg_0); + var varStart = il.Body.Variables.Count; + il.Body.Variables.Add(new VariableDefinition(module.ImportReference(guidType))); + il.Body.InitLocals = true; + il.Emit(OpCodes.Ldloca_S, (byte)varStart); + il.Emit(OpCodes.Ldtoken, valueType); + var getTypeFromHandle = typeType.Resolve().GetMethod("GetTypeFromHandle"); + il.Emit(OpCodes.Call, module.ImportReference(getTypeFromHandle)); + var tryFind = scriptingObjectType.Resolve().GetMethod("TryFind", 2); + il.Emit(OpCodes.Call, module.ImportReference(tryFind)); + il.Emit(OpCodes.Castclass, valueType); + il.Emit(OpCodes.Stfld, field); + } + } + else if (valueTypeDef.IsEnum) + { + // Replicate enum as bits + // TODO: use smaller uint depending on enum values range + if (serialize) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + var m = networkStreamType.GetMethod("WriteUInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + } + else + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + var m = networkStreamType.GetMethod("ReadUInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Stfld, field); + } + } + else if (valueType.IsValueType) + { + // Invoke structure generated serializer + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldflda, field); + il.Emit(OpCodes.Ldarg_1); + var m = valueTypeDef.GetMethod(serialize ? Thunk1 : Thunk2); + il.Emit(OpCodes.Call, module.ImportReference(m)); + } + else if (valueType.IsArray && valueType.GetElementType().IsValueType) + { + // TODO: support any array type by iterating over elements (separate serialize for each one) + var elementType = valueType.GetElementType(); + var varStart = il.Body.Variables.Count; + module.GetType("System.Int32", out var intType); + il.Body.Variables.Add(new VariableDefinition(intType)); + il.Body.Variables.Add(new VariableDefinition(new PointerType(elementType))); + il.Body.Variables.Add(new VariableDefinition(new PinnedType(valueType))); + il.Body.InitLocals = true; + if (serialize) + { + // [] array2 = Array1; + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + + // int num2 = ((array2 != null) ? array2.Length : 0); + il.Emit(OpCodes.Dup); + Instruction jmp1 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp1); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_I4_0); + Instruction jmp2 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp2); + il.Append(jmp1); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Append(jmp2); + il.Emit(OpCodes.Stloc, varStart + 0); + + // stream.WriteInt32(num2); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 0); + var m = networkStreamType.GetMethod("WriteInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + + // fixed (* bytes2 = Array1) + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, varStart + 2); + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brfalse_S, jmp3); + il.Emit(OpCodes.Ldloc_2); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + Instruction jmp4 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp4); + il.Append(jmp3); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); + Instruction jmp5 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp5); + + // stream.WriteBytes((byte*)bytes, num * sizeof())); + il.Append(jmp4); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelema, elementType); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); + il.Append(jmp5); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 1); + il.Emit(OpCodes.Ldloc, varStart + 0); + il.Emit(OpCodes.Sizeof, elementType); + il.Emit(OpCodes.Mul); + m = networkStreamType.GetMethod("WriteBytes", 2); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Stloc, varStart + 2); + } + else + { + // int num = stream.ReadInt32(); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_1); + var m = networkStreamType.GetMethod("ReadInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Stloc, varStart + 0); + + // System.Array.Resize(ref Array1, num); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldflda, field); + il.Emit(OpCodes.Ldloc, varStart + 0); + module.TryGetTypeReference("System.Array", out var arrayType); + m = arrayType.Resolve().GetMethod("Resize", 2); + il.Emit(OpCodes.Call, module.ImportReference(m.InflateGeneric(elementType))); + + // fixed (int* buffer = Array1) + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, varStart + 2); + Instruction jmp1 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brfalse_S, jmp1); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + Instruction jmp2 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp2); + il.Append(jmp1); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp3); + + // stream.ReadBytes((byte*)buffer, num * sizeof()); + il.Append(jmp2); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelema, elementType); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); + il.Append(jmp3); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 1); + il.Emit(OpCodes.Ldloc, varStart + 0); + il.Emit(OpCodes.Sizeof, elementType); + il.Emit(OpCodes.Mul); + m = networkStreamType.GetMethod("ReadBytes", 2); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Stloc, varStart + 2); + } + } + else + { + // Unknown type + Log.Error($"Not supported type '{valueType.FullName}' on {field.Name} in {type.FullName} for automatic replication."); + failed = true; + } } } }