Add automatic network serializers generation for NetworkReplicated fields in C++ types
This commit is contained in:
@@ -96,6 +96,18 @@ void NetworkReplicationService::Dispose()
|
||||
|
||||
NetworkReplicationService NetworkReplicationServiceInstance;
|
||||
|
||||
Dictionary<ScriptingTypeHandle, NetworkReplicator::SerializeFuncPair> NetworkReplicator::SerializersTable;
|
||||
|
||||
void INetworkSerializable_Serialize(void* instance, NetworkStream* stream)
|
||||
{
|
||||
((INetworkSerializable*)instance)->Serialize(stream);
|
||||
}
|
||||
|
||||
void INetworkSerializable_Deserialize(void* instance, NetworkStream* stream)
|
||||
{
|
||||
((INetworkSerializable*)instance)->Deserialize(stream);
|
||||
}
|
||||
|
||||
NetworkReplicatedObject* ResolveObject(Guid objectId, Guid ownerId, char objectTypeName[128])
|
||||
{
|
||||
// Lookup object
|
||||
@@ -131,6 +143,25 @@ NetworkReplicatedObject* ResolveObject(Guid objectId, Guid ownerId, char objectT
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
NetworkReplicator::SerializeFuncPair NetworkReplicator::GetSerializer(const ScriptingTypeHandle& typeHandle)
|
||||
{
|
||||
// Get serializers pair from table
|
||||
SerializeFuncPair result(nullptr, nullptr);
|
||||
if (!SerializersTable.TryGet(typeHandle, result))
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* owner)
|
||||
{
|
||||
if (!obj || NetworkManager::State == NetworkConnectionState::Offline)
|
||||
@@ -215,11 +246,11 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
}
|
||||
|
||||
// Serialize object
|
||||
// TODO: cache per-type serialization thunk to boost CPU performance
|
||||
stream->Initialize();
|
||||
if (auto* serializable = ScriptingObject::ToInterface<INetworkSerializable>(obj))
|
||||
const auto serializerFunc = NetworkReplicator::GetSerializer(obj->GetTypeHandle()).First;
|
||||
if (serializerFunc)
|
||||
{
|
||||
serializable->Serialize(stream);
|
||||
serializerFunc(obj, stream);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -227,7 +258,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
if (!item.InvalidTypeWarn)
|
||||
{
|
||||
item.InvalidTypeWarn = true;
|
||||
LOG(Error, "[NetworkReplicator] Cannot serialize object {} (missing serialization logic)", item.ToString());
|
||||
LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
|
||||
}
|
||||
#endif
|
||||
continue;
|
||||
@@ -279,10 +310,10 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw
|
||||
stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.DataSize);
|
||||
|
||||
// Deserialize object
|
||||
// TODO: cache per-type serialization thunk to boost CPU performance
|
||||
if (auto* serializable = ScriptingObject::ToInterface<INetworkSerializable>(obj))
|
||||
const auto deserializerFunc = NetworkReplicator::GetSerializer(obj->GetTypeHandle()).Second;
|
||||
if (deserializerFunc)
|
||||
{
|
||||
serializable->Deserialize(stream);
|
||||
deserializerFunc(obj, stream);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -290,7 +321,7 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw
|
||||
if (!item.InvalidTypeWarn)
|
||||
{
|
||||
item.InvalidTypeWarn = true;
|
||||
LOG(Error, "[NetworkReplicator] Cannot serialize object {} (missing serialization logic)", item.ToString());
|
||||
LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#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"
|
||||
|
||||
@@ -12,6 +14,17 @@
|
||||
API_CLASS(static, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicator
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicator);
|
||||
|
||||
public:
|
||||
typedef void (*SerializeFunc)(void* instance, NetworkStream* stream);
|
||||
typedef Pair<SerializeFunc, SerializeFunc> SerializeFuncPair;
|
||||
static SerializeFuncPair GetSerializer(const ScriptingTypeHandle& typeHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Global table for registered types serialization methods (key is type name, value is pair of methods to serialize and deserialize object).
|
||||
/// </summary>
|
||||
static Dictionary<ScriptingTypeHandle, SerializeFuncPair> SerializersTable;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Adds the object to the network replication system.
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace Flax.Build.Bindings
|
||||
ApiTypeInfo apiType = null;
|
||||
if (dot != -1)
|
||||
{
|
||||
var type = new TypeInfo { Type = value.Substring(0, dot) };
|
||||
var type = new TypeInfo(value.Substring(0, dot));
|
||||
apiType = FindApiTypeInfo(buildData, type, caller);
|
||||
}
|
||||
|
||||
@@ -697,7 +697,7 @@ namespace Flax.Build.Bindings
|
||||
contents.Append("unsafe partial class ").Append(classInfo.Name);
|
||||
var hasBase = classInfo.BaseType != null && !classInfo.IsBaseTypeHidden;
|
||||
if (hasBase)
|
||||
contents.Append(" : ").Append(GenerateCSharpNativeToManaged(buildData, new TypeInfo { Type = classInfo.BaseType.Name }, classInfo));
|
||||
contents.Append(" : ").Append(GenerateCSharpNativeToManaged(buildData, new TypeInfo(classInfo.BaseType), classInfo));
|
||||
var hasInterface = false;
|
||||
if (classInfo.Interfaces != null)
|
||||
{
|
||||
@@ -1142,7 +1142,7 @@ namespace Flax.Build.Bindings
|
||||
contents.Append("private ");
|
||||
contents.Append("unsafe partial struct ").Append(structureInfo.Name);
|
||||
if (structureInfo.BaseType != null && structureInfo.IsPod)
|
||||
contents.Append(" : ").Append(GenerateCSharpNativeToManaged(buildData, new TypeInfo { Type = structureInfo.BaseType.Name }, structureInfo));
|
||||
contents.Append(" : ").Append(GenerateCSharpNativeToManaged(buildData, new TypeInfo(structureInfo.BaseType), structureInfo));
|
||||
contents.AppendLine();
|
||||
contents.Append(indent + "{");
|
||||
indent += " ";
|
||||
|
||||
@@ -55,6 +55,11 @@ namespace Flax.Build.Bindings
|
||||
GenericArgs = other.GenericArgs != null ? new List<TypeInfo>(other.GenericArgs) : null;
|
||||
}
|
||||
|
||||
public TypeInfo(ApiTypeInfo apiTypeInfo)
|
||||
{
|
||||
Type = apiTypeInfo.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inflates the type with typedefs for generic arguments.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using Flax.Build.Bindings;
|
||||
|
||||
namespace Flax.Build.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Flax.Build plugin for Networking extenrions support. Generates required bindings glue code for automatic types replication and RPCs invoking.
|
||||
/// </summary>
|
||||
/// <seealso cref="Flax.Build.Plugin" />
|
||||
internal sealed class NetworkingPlugin : Plugin
|
||||
{
|
||||
internal const string NetworkReplicated = "NetworkReplicated";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
|
||||
BindingsGenerator.ParseMemberTag += OnParseMemberTag;
|
||||
BindingsGenerator.GenerateCppTypeInternals += OnGenerateCppTypeInternals;
|
||||
BindingsGenerator.GenerateCppTypeInitRuntime += OnGenerateCppTypeInitRuntime;
|
||||
}
|
||||
|
||||
private void OnParseMemberTag(ref bool valid, BindingsGenerator.TagParameter tag, MemberInfo memberInfo)
|
||||
{
|
||||
if (tag.Tag != NetworkReplicated)
|
||||
return;
|
||||
if (memberInfo is FieldInfo)
|
||||
{
|
||||
// Mark member as replicated
|
||||
valid = true;
|
||||
memberInfo.SetTag(NetworkReplicated, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGenerateCppTypeInternals(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents)
|
||||
{
|
||||
// Skip modules that don't use networking
|
||||
var module = BindingsGenerator.CurrentModule;
|
||||
if (module.GetTag(NetworkReplicated) == null)
|
||||
return;
|
||||
|
||||
// Check if type uses automated network replication
|
||||
List<FieldInfo> fields = null;
|
||||
if (typeInfo is ClassInfo classInfo)
|
||||
{
|
||||
fields = classInfo.Fields;
|
||||
// TODO: add support for class properties replication
|
||||
}
|
||||
else if (typeInfo is StructureInfo structInfo)
|
||||
{
|
||||
fields = structInfo.Fields;
|
||||
}
|
||||
bool useReplication = false;
|
||||
if (fields != null)
|
||||
{
|
||||
foreach (var fieldInfo in fields)
|
||||
{
|
||||
if (fieldInfo.GetTag(NetworkReplicated) != null)
|
||||
{
|
||||
useReplication = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!useReplication)
|
||||
return;
|
||||
|
||||
typeInfo.SetTag(NetworkReplicated, string.Empty);
|
||||
|
||||
// Generate C++ wrapper functions to serialize/deserialize type
|
||||
BindingsGenerator.CppIncludeFiles.Add("Engine/Networking/NetworkReplicator.h");
|
||||
BindingsGenerator.CppIncludeFiles.Add("Engine/Networking/NetworkStream.h");
|
||||
OnGenerateCppTypeSerialize(buildData, typeInfo, contents, fields, true);
|
||||
OnGenerateCppTypeSerialize(buildData, typeInfo, contents, fields, false);
|
||||
}
|
||||
|
||||
private void OnGenerateCppTypeSerialize(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents, List<FieldInfo> 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.AppendLine(" {");
|
||||
contents.AppendLine($" {typeInfo.NativeName}& object = *({typeInfo.NativeName}*)instance;");
|
||||
if (IsRawPOD(buildData, typeInfo))
|
||||
{
|
||||
// POD types as raw bytes
|
||||
OnGenerateCppWriteRaw(contents, "object", serialize);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (typeInfo is ClassStructInfo classStructInfo && classStructInfo.BaseType != null && classStructInfo.BaseType.Name != "ScriptingObject")
|
||||
{
|
||||
// Replicate base type
|
||||
OnGenerateCppWriteSerializer(contents, classStructInfo.BaseType.NativeName, "object", serialize);
|
||||
}
|
||||
|
||||
// Replicate all marked fields
|
||||
if (fields != null)
|
||||
{
|
||||
foreach (var fieldInfo in fields)
|
||||
{
|
||||
if (fieldInfo.GetTag(NetworkReplicated) == null)
|
||||
continue;
|
||||
OnGenerateCppTypeSerializeData(buildData, typeInfo, contents, fieldInfo.Type, $"object.{fieldInfo.Name}", serialize);
|
||||
}
|
||||
}
|
||||
// TODO: add support for class properties replication
|
||||
}
|
||||
contents.AppendLine(" }");
|
||||
contents.AppendLine();
|
||||
}
|
||||
|
||||
private bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo type)
|
||||
{
|
||||
// TODO: what if type fields have custom replication settings (eg. compression)?
|
||||
type.EnsureInited(buildData);
|
||||
return type.IsPod;
|
||||
}
|
||||
|
||||
private bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo caller, ApiTypeInfo apiType, TypeInfo type)
|
||||
{
|
||||
if (type.IsPod(buildData, caller))
|
||||
{
|
||||
if (apiType != null)
|
||||
return IsRawPOD(buildData, apiType);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnGenerateCppTypeSerializeData(Builder.BuildData buildData, ApiTypeInfo caller, StringBuilder contents, TypeInfo type, string name, bool serialize)
|
||||
{
|
||||
var apiType = BindingsGenerator.FindApiTypeInfo(buildData, type, caller);
|
||||
if (apiType == null || IsRawPOD(buildData, caller, apiType, type))
|
||||
{
|
||||
// POD types as raw bytes
|
||||
if (type.IsPtr)
|
||||
throw new Exception($"Invalid pointer type '{type}' that cannot be serialized for replication of {caller.Name}.");
|
||||
if (type.IsRef)
|
||||
throw new Exception($"Invalid reference type '{type}' that cannot be serialized for replication of {caller.Name}.");
|
||||
OnGenerateCppWriteRaw(contents, "&" + name, serialize);
|
||||
}
|
||||
else if (apiType.IsScriptingObject)
|
||||
{
|
||||
// Object ID
|
||||
if (serialize)
|
||||
{
|
||||
contents.AppendLine($" {{Guid id = {name} ? {name}->GetID() : Guid::Empty;");
|
||||
OnGenerateCppWriteRaw(contents, "&id", serialize);
|
||||
contents.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
contents.AppendLine($" {{Guid id;");
|
||||
OnGenerateCppWriteRaw(contents, "&id", serialize);
|
||||
contents.AppendLine($" {name} = Scripting::TryFindObject(id);}}");
|
||||
}
|
||||
}
|
||||
else if (apiType.IsStruct)
|
||||
{
|
||||
if (type.IsPtr)
|
||||
throw new Exception($"Invalid pointer type '{type}' that cannot be serialized for replication of {caller.Name}.");
|
||||
if (type.IsRef)
|
||||
throw new Exception($"Invalid reference type '{type}' that cannot be serialized for replication of {caller.Name}.");
|
||||
|
||||
// Structure serializer
|
||||
OnGenerateCppWriteSerializer(contents, apiType.NativeName, name, serialize);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In-built serialization route (compiler will warn if type is not supported)
|
||||
OnGenerateCppWriteRaw(contents, name, serialize);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGenerateCppWriteRaw(StringBuilder contents, string data, bool serialize)
|
||||
{
|
||||
var method = serialize ? "Write" : "Read";
|
||||
contents.AppendLine($" stream->{method}({data});");
|
||||
}
|
||||
|
||||
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);}}");
|
||||
}
|
||||
|
||||
private void OnGenerateCppTypeInitRuntime(Builder.BuildData buildData, ApiTypeInfo typeInfo, StringBuilder contents)
|
||||
{
|
||||
if (typeInfo.GetTag(NetworkReplicated) == null)
|
||||
return;
|
||||
var typeNameNative = typeInfo.FullNameNative;
|
||||
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);");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@
|
||||
<Compile Include="Build\NativeCpp\IncludesCache.cs" />
|
||||
<Compile Include="Build\NativeCpp\LinkEnvironment.cs" />
|
||||
<Compile Include="Build\Plugin.cs" />
|
||||
<Compile Include="Build\Plugins\NetworkingPlugin.cs" />
|
||||
<Compile Include="Build\Plugins\VisualScriptingPlugin.cs" />
|
||||
<Compile Include="Build\Profiling.cs" />
|
||||
<Compile Include="Build\ProjectTarget.cs" />
|
||||
|
||||
Reference in New Issue
Block a user