Files
FlaxEngine/Source/Engine/Networking/NetworkReplicator.cpp

2431 lines
86 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "NetworkReplicator.h"
#include "NetworkClient.h"
#include "NetworkManager.h"
#include "NetworkInternal.h"
#include "NetworkStream.h"
#include "NetworkMessage.h"
#include "NetworkPeer.h"
#include "NetworkChannelType.h"
#include "NetworkEvent.h"
#include "NetworkRpc.h"
#include "INetworkSerializable.h"
#include "INetworkObject.h"
#include "NetworkReplicationHierarchy.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Core/Types/DataContainer.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Level/Actor.h"
#include "Engine/Level/SceneObject.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Threading/ThreadLocal.h"
#if USE_EDITOR
#include "FlaxEngine.Gen.h"
#endif
#if !BUILD_RELEASE
bool NetworkReplicator::EnableLog = false;
#include "Engine/Core/Log.h"
#include "Engine/Content/Content.h"
#define NETWORK_REPLICATOR_LOG(messageType, format, ...) if (NetworkReplicator::EnableLog) { LOG(messageType, format, ##__VA_ARGS__); }
#define USE_NETWORK_REPLICATOR_LOG 1
#else
#define NETWORK_REPLICATOR_LOG(messageType, format, ...)
#endif
#if COMPILE_WITH_PROFILER
bool NetworkInternal::EnableProfiling = false;
Dictionary<Pair<ScriptingTypeHandle, StringAnsiView>, NetworkInternal::ProfilerEvent> NetworkInternal::ProfilerEvents;
#endif
PACK_STRUCT(struct NetworkMessageObjectReplicate
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicate;
uint32 OwnerFrame;
});
PACK_STRUCT(struct NetworkMessageObjectPartPayload
{
uint16 DataSize;
uint16 PartsCount;
uint16 PartSize;
});
PACK_STRUCT(struct NetworkMessageObjectPart
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicatePart;
uint32 OwnerFrame;
uint16 DataSize;
uint16 PartsCount;
uint16 PartStart;
uint16 PartSize;
});
PACK_STRUCT(struct NetworkMessageObjectSpawn
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
uint32 OwnerClientId;
uint32 OwnerSpawnId; // Unique for peer who spawned it and matches OwnerSpawnId inside following part messages
uint16 ItemsCount; // Total items count
uint8 UseParts : 1; // True if spawn message is header-only and all items come in the separate parts
});
PACK_STRUCT(struct NetworkMessageObjectSpawnPart
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawnPart;
uint32 OwnerClientId;
uint32 OwnerSpawnId;
});
// TODO: optimize spawn item to use Network Keys rather than fixed-size data
PACK_STRUCT(struct NetworkMessageObjectSpawnItem
{
Guid ObjectId;
Guid ParentId;
Guid PrefabObjectID;
char ObjectTypeName[128];
});
PACK_STRUCT(struct NetworkMessageObjectDespawn
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectDespawn;
});
PACK_STRUCT(struct NetworkMessageObjectRole
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectRole;
uint32 OwnerClientId;
});
PACK_STRUCT(struct NetworkMessageObjectRpc
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectRpc;
uint32 OwnerFrame;
});
struct NetworkReplicatedObject
{
struct
{
NetworkClientsMask Mask;
BytesContainer Data;
void Clear()
{
Mask = NetworkClientsMask();
Data.Release();
}
} RepCache;
ScriptingObjectReference<ScriptingObject> Object;
Guid ObjectId;
Guid ParentId;
DataContainer<uint32> TargetClientIds;
INetworkObject* AsNetworkObject;
uint32 OwnerClientId;
uint32 LastOwnerFrame = 0;
NetworkObjectRole Role;
uint8 Spawned : 1;
uint8 Synced : 1;
NetworkReplicatedObject()
{
Spawned = 0;
Synced = 0;
}
void Dirty()
{
RepCache.Mask = NetworkClientsMask();
}
bool operator==(const NetworkReplicatedObject& other) const
{
return ObjectId == other.ObjectId;
}
bool operator==(const ScriptingObject* other) const
{
return other && ObjectId == other->GetID();
}
bool operator==(const Guid& other) const
{
return ObjectId == other;
}
String ToString() const
{
return ObjectId.ToString();
}
};
inline uint32 GetHash(const NetworkReplicatedObject& key)
{
return GetHash(key.ObjectId);
}
inline uint32 GetHash(const ScriptingObject* key)
{
return key ? GetHash(key->GetID()) : 0;
}
struct Serializer
{
NetworkReplicator::SerializeFunc Methods[2];
void* Tags[2];
};
struct PartsItem
{
ScriptingObjectReference<ScriptingObject> Object;
Guid ObjectId;
uint16 PartsLeft;
uint32 OwnerFrame;
uint32 OwnerClientId;
const void* Tag;
Array<byte> Data;
};
struct SpawnItem
{
ScriptingObjectReference<ScriptingObject> Object;
DataContainer<uint32> Targets;
bool HasOwnership = false;
bool HierarchicalOwnership = false;
uint32 OwnerClientId;
NetworkObjectRole Role;
};
struct SpawnItemParts
{
NetworkMessageObjectSpawn MsgData;
Guid PrefabId;
Array<NetworkMessageObjectSpawnItem> Items;
};
struct SpawnGroup
{
Array<SpawnItem*, InlinedAllocation<8>> Items;
};
struct DespawnItem
{
Guid Id;
DataContainer<uint32> Targets;
};
struct RpcSendItem
{
ScriptingObjectReference<ScriptingObject> Object;
NetworkRpcName Name;
NetworkRpcInfo Info;
BytesContainer ArgsData;
DataContainer<uint32> Targets;
};
namespace
{
CriticalSection ObjectsLock;
HashSet<NetworkReplicatedObject> Objects;
Array<PartsItem> ReplicationParts;
Array<PartsItem> RpcParts;
Array<SpawnItemParts> SpawnParts;
Array<SpawnItem> SpawnQueue;
Array<DespawnItem> DespawnQueue;
Array<RpcSendItem> RpcQueue;
Dictionary<Guid, Guid> IdsRemappingTable;
NetworkStream* CachedWriteStream = nullptr;
NetworkStream* CachedReadStream = nullptr;
NetworkReplicationHierarchyUpdateResult* CachedReplicationResult = nullptr;
NetworkReplicationHierarchy* Hierarchy = nullptr;
Array<NetworkClient*> NewClients;
Array<NetworkConnection> CachedTargets;
Dictionary<ScriptingTypeHandle, Serializer> SerializersTable;
#if !COMPILE_WITHOUT_CSHARP
Dictionary<StringAnsiView, StringAnsi*> CSharpCachedNames;
#endif
Array<Guid> DespawnedObjects;
uint32 SpawnId = 0;
uint32 RpcId = 0;
#if USE_EDITOR
void OnScriptsReloading()
{
ScopeLock lock(ObjectsLock);
if (Objects.HasItems())
LOG(Warning, "Hot-reloading scripts with network objects active.");
if (Hierarchy)
{
Delete(Hierarchy);
Hierarchy = nullptr;
}
// Clear any references to non-engine scripts before code hot-reload
BinaryModule* flaxModule = GetBinaryModuleFlaxEngine();
for (auto i = SerializersTable.Begin(); i.IsNotEnd(); ++i)
{
if (i->Key.Module != flaxModule)
SerializersTable.Remove(i);
}
for (auto i = NetworkRpcInfo::RPCsTable.Begin(); i.IsNotEnd(); ++i)
{
if (i->Key.First.Module != flaxModule)
NetworkRpcInfo::RPCsTable.Remove(i);
}
}
#endif
}
class NetworkReplicationService : public EngineService
{
public:
NetworkReplicationService()
: EngineService(TEXT("Network Replication"), 1100)
{
}
bool Init() override;
void Dispose() override;
};
bool NetworkReplicationService::Init()
{
#if USE_EDITOR
Scripting::ScriptsReloading.Bind(OnScriptsReloading);
#endif
return false;
}
void NetworkReplicationService::Dispose()
{
NetworkInternal::NetworkReplicatorClear();
#if !COMPILE_WITHOUT_CSHARP
CSharpCachedNames.ClearDelete();
#endif
}
NetworkReplicationService NetworkReplicationServiceInstance;
void INetworkSerializable_Native_Serialize(void* instance, NetworkStream* stream, void* tag)
{
const int16 vtableOffset = (int16)(intptr)tag;
((INetworkSerializable*)((byte*)instance + vtableOffset))->Serialize(stream);
}
void INetworkSerializable_Native_Deserialize(void* instance, NetworkStream* stream, void* tag)
{
const int16 vtableOffset = (int16)(intptr)tag;
((INetworkSerializable*)((byte*)instance + vtableOffset))->Deserialize(stream);
}
void INetworkSerializable_Script_Serialize(void* instance, NetworkStream* stream, void* tag)
{
auto obj = (ScriptingObject*)instance;
auto interface = ScriptingObject::ToInterface<INetworkSerializable>(obj);
interface->Serialize(stream);
}
void INetworkSerializable_Script_Deserialize(void* instance, NetworkStream* stream, void* tag)
{
auto obj = (ScriptingObject*)instance;
auto interface = ScriptingObject::ToInterface<INetworkSerializable>(obj);
interface->Deserialize(stream);
}
NetworkReplicatedObject* ResolveObject(Guid objectId)
{
auto it = Objects.Find(objectId);
if (it != Objects.End())
return &it->Item;
IdsRemappingTable.TryGet(objectId, objectId);
it = Objects.Find(objectId);
return it != Objects.End() ? &it->Item : nullptr;
}
NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, const StringAnsiView& objectTypeName)
{
// Lookup object
NetworkReplicatedObject* obj = ResolveObject(objectId);
if (obj)
return obj;
// Try to find the object within the same parent (eg. spawned locally on both client and server)
IdsRemappingTable.TryGet(parentId, parentId);
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(objectTypeName);
if (!objectType)
return nullptr;
for (auto& e : Objects)
{
auto& item = e.Item;
const ScriptingObject* obj = item.Object.Get();
if (item.LastOwnerFrame == 0 &&
item.ParentId == parentId &&
obj &&
obj->GetTypeHandle() == objectType &&
!IdsRemappingTable.ContainsValue(item.ObjectId))
{
if (NetworkManager::IsClient())
{
// Boost future lookups by using indirection
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", objectId, item.ToString(), obj->GetType().ToString());
IdsRemappingTable.Add(objectId, item.ObjectId);
}
return &item;
}
}
return nullptr;
}
void BuildCachedTargets(const Array<NetworkClient*>& clients)
{
CachedTargets.Clear();
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected)
CachedTargets.Add(client->Connection);
}
}
void BuildCachedTargets(const Array<NetworkClient*>& clients, const NetworkClient* excludedClient)
{
CachedTargets.Clear();
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected && client != excludedClient)
CachedTargets.Add(client->Connection);
}
}
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId, const NetworkClientsMask clientsMask = NetworkClientsMask::All)
{
CachedTargets.Clear();
if (clientIds.IsValid())
{
for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++)
{
const NetworkClient* client = clients.Get()[clientIndex];
if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex))
{
for (int32 i = 0; i < clientIds.Length(); i++)
{
if (clientIds[i] == client->ClientId)
{
CachedTargets.Add(client->Connection);
break;
}
}
}
}
}
else
{
for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++)
{
const NetworkClient* client = clients.Get()[clientIndex];
if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex))
CachedTargets.Add(client->Connection);
}
}
}
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& clientIds1, const Span<uint32>& clientIds2, const uint32 excludedClientId = NetworkManager::ServerClientId)
{
CachedTargets.Clear();
if (clientIds1.IsValid())
{
if (clientIds2.IsValid())
{
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId)
{
for (int32 i = 0; i < clientIds1.Length(); i++)
{
if (clientIds1[i] == client->ClientId)
{
for (int32 j = 0; j < clientIds2.Length(); j++)
{
if (clientIds2[j] == client->ClientId)
{
CachedTargets.Add(client->Connection);
break;
}
}
break;
}
}
}
}
}
else
{
BuildCachedTargets(clients, clientIds1, excludedClientId);
}
}
else
{
BuildCachedTargets(clients, clientIds2, excludedClientId);
}
}
FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item, const NetworkClientsMask clientsMask = NetworkClientsMask::All)
{
// By default send object to all connected clients excluding the owner but with optional TargetClientIds list
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId, clientsMask);
}
void SetupObjectSpawnMessageItem(SpawnItem* e, NetworkMessage& msg)
{
ScriptingObject* obj = e->Object.Get();
auto it = Objects.Find(obj->GetID());
const auto& item = it->Item;
// Add object into spawn message
NetworkMessageObjectSpawnItem msgDataItem;
msgDataItem.ObjectId = item.ObjectId;
msgDataItem.ParentId = item.ParentId;
{
// Remap local client object ids into server ids
IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
}
msgDataItem.PrefabObjectID = Guid::Empty;
auto* objScene = ScriptingObject::Cast<SceneObject>(obj);
if (objScene && objScene->HasPrefabLink())
msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
const StringAnsiView objectTypeName = obj->GetType().Fullname;
Platform::MemoryCopy(msgDataItem.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
msgDataItem.ObjectTypeName[objectTypeName.Length()] = 0;
msg.WriteStructure(msgDataItem);
}
void SendInParts(NetworkPeer* peer, NetworkChannelType channel, const byte* data, const uint16 dataSize, NetworkMessage& msg, const NetworkRpcName& name, bool toServer, const Guid& objectId, uint32 ownerFrame, NetworkMessageIDs partId)
{
NetworkMessageObjectPartPayload msgDataPayload;
msgDataPayload.DataSize = dataSize;
const uint32 networkKeyIdWorstCaseSize = sizeof(uint32) + sizeof(Guid);
const uint32 msgMaxData = peer->Config.MessageSize - msg.Position - sizeof(NetworkMessageObjectPartPayload);
const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectPart) - networkKeyIdWorstCaseSize;
uint32 partsCount = 1;
uint32 dataStart = 0;
uint32 msgDataSize = dataSize;
if (dataSize > msgMaxData)
{
// Send msgMaxData within first message
msgDataSize = msgMaxData;
dataStart += msgMaxData;
// Send rest of the data in separate parts
partsCount += Math::DivideAndRoundUp(dataSize - dataStart, partMaxData);
// TODO: promote channel to Ordered when using parts?
}
else
dataStart += dataSize;
ASSERT(partsCount <= MAX_uint8);
msgDataPayload.PartsCount = partsCount;
msgDataPayload.PartSize = msgDataSize;
msg.WriteStructure(msgDataPayload);
msg.WriteBytes(data, msgDataSize);
uint32 messageSize = msg.Length;
if (toServer)
peer->EndSendMessage(channel, msg);
else
peer->EndSendMessage(channel, msg, CachedTargets);
// Send all other parts
for (uint32 partIndex = 1; partIndex < partsCount; partIndex++)
{
NetworkMessageObjectPart msgDataPart;
msgDataPart.ID = partId;
msgDataPart.OwnerFrame = ownerFrame;
msgDataPart.DataSize = msgDataPayload.DataSize;
msgDataPart.PartsCount = msgDataPayload.PartsCount;
msgDataPart.PartStart = dataStart;
msgDataPart.PartSize = Math::Min(dataSize - dataStart, partMaxData);
msg = peer->BeginSendMessage();
msg.WriteStructure(msgDataPart);
msg.WriteNetworkId(objectId);
msg.WriteBytes(data + msgDataPart.PartStart, msgDataPart.PartSize);
messageSize += msg.Length;
dataStart += msgDataPart.PartSize;
if (toServer)
peer->EndSendMessage(channel, msg);
else
peer->EndSendMessage(channel, msg, CachedTargets);
}
ASSERT_LOW_LAYER(dataStart == dataSize);
#if COMPILE_WITH_PROFILER
// Network stats recording
if (NetworkInternal::EnableProfiling)
{
auto& profileEvent = NetworkInternal::ProfilerEvents[name];
profileEvent.Count++;
profileEvent.DataSize += dataSize;
profileEvent.MessageSize += messageSize;
profileEvent.Receivers += toServer ? 1 : CachedTargets.Count();
}
#endif
}
void SendObjectSpawnMessage(const SpawnGroup& group, const Array<NetworkClient*>& clients)
{
PROFILE_CPU();
const bool isClient = NetworkManager::IsClient();
auto* peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
NetworkMessageObjectSpawn msgData;
msgData.ItemsCount = group.Items.Count();
Guid prefabId;
{
// The first object is a root of the group (eg. prefab instance root actor)
SpawnItem* e = group.Items[0];
ScriptingObject* obj = e->Object.Get();
msgData.OwnerClientId = e->OwnerClientId;
auto* objScene = ScriptingObject::Cast<SceneObject>(obj);
prefabId = objScene && objScene->HasPrefabLink() ? objScene->GetPrefabID() : Guid::Empty;
// Setup clients that should receive this spawn message
auto it = Objects.Find(obj->GetID());
const auto& item = it->Item;
BuildCachedTargets(clients, item.TargetClientIds);
}
// Network Peer has fixed size of messages so split spawn message into parts if there are too many objects to fit at once
msgData.OwnerSpawnId = ++SpawnId;
msgData.UseParts = msg.BufferSize - msg.Position < group.Items.Count() * sizeof(NetworkMessageObjectSpawnItem);
msg.WriteStructure(msgData);
msg.WriteNetworkId(prefabId);
if (msgData.UseParts)
{
if (isClient)
peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
// Send spawn items in separate parts
NetworkMessageObjectSpawnPart msgDataPart;
msgDataPart.OwnerClientId = msgData.OwnerClientId;
msgDataPart.OwnerSpawnId = msgData.OwnerSpawnId;
uint16 itemIndex = 0;
constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
while (itemIndex < msgData.ItemsCount)
{
msg = peer->BeginSendMessage();
msg.WriteStructure(msgDataPart);
// Write as many items as possible into this message
while (msg.Position + spawnItemMaxSize <= msg.BufferSize && itemIndex < msgData.ItemsCount)
{
msg.WriteUInt16(itemIndex);
SetupObjectSpawnMessageItem(group.Items[itemIndex], msg);
itemIndex++;
}
if (isClient)
peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
}
}
else
{
// Send all spawn items within the spawn message
for (SpawnItem* e : group.Items)
SetupObjectSpawnMessageItem(e, msg);
if (isClient)
peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
}
}
void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkClient* excludedClient = nullptr)
{
NetworkMessageObjectRole msgData;
Guid objectId = item.ObjectId;
IdsRemappingTable.KeyOf(objectId, &objectId);
msgData.OwnerClientId = item.OwnerClientId;
auto peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteNetworkId(objectId);
if (NetworkManager::IsClient())
{
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
}
else
{
BuildCachedTargets(NetworkManager::Clients, excludedClient);
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
}
}
void SendDespawn(DespawnItem& e)
{
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString());
NetworkMessageObjectDespawn msgData;
Guid objectId = e.Id;
{
// Remap local client object ids into server ids
IdsRemappingTable.KeyOf(objectId, &objectId);
}
auto peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteNetworkId(objectId);
BuildCachedTargets(NetworkManager::Clients, e.Targets);
if (NetworkManager::IsClient())
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
else
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
}
void SendReplication(ScriptingObject* obj, NetworkClientsMask targetClients)
{
auto it = Objects.Find(obj->GetID());
if (it.IsEnd())
return;
auto& item = it->Item;
const bool isClient = NetworkManager::IsClient();
// Skip serialization of objects that none will receive
if (!isClient)
{
BuildCachedTargets(item, targetClients);
if (CachedTargets.Count() == 0)
return;
}
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkSerialize();
// Serialize object
NetworkStream* stream = CachedWriteStream;
stream->Initialize();
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
if (failed)
{
//NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
return;
}
const uint32 size = stream->GetPosition();
if (size > MAX_uint16)
{
LOG(Error, "Too much data for object {} replication ({} bytes provided while limit is {}).", item.ToString(), size, MAX_uint16);
return;
}
#if USE_NETWORK_REPLICATOR_CACHE
// Process replication cache to skip sending object data if it didn't change
if (item.RepCache.Data.Length() == size &&
item.RepCache.Mask == targetClients &&
Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0)
{
return;
}
item.RepCache.Mask = targetClients;
item.RepCache.Data.Copy(stream->GetBuffer(), size);
#endif
// TODO: use Unreliable for dynamic objects that are replicated every frame? (eg. player state)
constexpr NetworkChannelType repChannel = NetworkChannelType::Reliable;
// Send object to clients
NetworkMessageObjectReplicate msgData;
msgData.OwnerFrame = NetworkManager::Frame;
Guid objectId = item.ObjectId, parentId = item.ParentId;
{
// Remap local client object ids into server ids
IdsRemappingTable.KeyOf(objectId, &objectId);
IdsRemappingTable.KeyOf(parentId, &parentId);
}
NetworkPeer* peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteNetworkId(objectId);
msg.WriteNetworkId(parentId);
msg.WriteNetworkName(obj->GetType().Fullname);
const NetworkRpcName name(obj->GetTypeHandle(), StringAnsiView::Empty);
SendInParts(peer, repChannel, stream->GetBuffer(), size, msg, name, isClient, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectReplicatePart);
}
void SendRpc(RpcSendItem& e)
{
ScriptingObject* obj = e.Object.Get();
if (!obj)
return;
auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
{
#if !BUILD_RELEASE
if (!DespawnedObjects.Contains(obj->GetID()))
LOG(Error, "Cannot invoke RPC method '{0}.{1}' on object '{2}' that is not registered in networking (use 'NetworkReplicator.AddObject').", e.Name.First.ToString(), e.Name.Second.ToString(), obj->GetID());
#endif
return;
}
auto& item = it->Item;
if (e.ArgsData.Length() > MAX_uint16)
{
LOG(Error, "Too much data for object RPC method '{}.{}' on object '{}' ({} bytes provided while limit is {}).", e.Name.First.ToString(), e.Name.Second.ToString(), obj->GetID(), e.ArgsData.Length(), MAX_uint16);
return;
}
const NetworkManagerMode mode = NetworkManager::Mode;
NetworkPeer* peer = NetworkManager::Peer;
bool toServer;
if (e.Info.Server && mode == NetworkManagerMode::Client)
{
// Client -> Server
#if USE_NETWORK_REPLICATOR_LOG
if (e.Targets.Length() != 0)
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}.{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString());
#endif
toServer = true;
}
else if (e.Info.Client && (mode == NetworkManagerMode::Server || mode == NetworkManagerMode::Host))
{
// Server -> Client(s)
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId);
if (CachedTargets.IsEmpty())
return;
toServer = false;
}
else
return;
// Send RPC message
//NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}.{} object ID={}", e.Name.First.ToString(), e.Name.Second.ToString(), item.ToString());
NetworkMessageObjectRpc msgData;
msgData.OwnerFrame = ++RpcId;
Guid objectId = item.ObjectId;
Guid parentId = item.ParentId;
{
// Remap local client object ids into server ids
IdsRemappingTable.KeyOf(objectId, &objectId);
IdsRemappingTable.KeyOf(parentId, &parentId);
}
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteNetworkId(objectId);
msg.WriteNetworkId(parentId);
msg.WriteNetworkName(obj->GetType().Fullname);
msg.WriteNetworkName(e.Name.First.GetType().Fullname);
msg.WriteNetworkName(e.Name.Second);
NetworkChannelType channel = (NetworkChannelType)e.Info.Channel;
SendInParts(peer, channel, e.ArgsData.Get(), e.ArgsData.Length(), msg, e.Name, toServer, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectRpcPart);
}
void DeleteNetworkObject(ScriptingObject* obj)
{
// Remove from the mapping table
const Guid id = obj->GetID();
IdsRemappingTable.Remove(id);
IdsRemappingTable.RemoveValue(id);
if (obj->Is<Script>() && ((Script*)obj)->GetParent())
((Script*)obj)->GetParent()->DeleteObject();
else
obj->DeleteObject();
}
bool IsParentOf(ScriptingObject* obj, ScriptingObject* parent)
{
if (const auto* sceneObject = ScriptingObject::Cast<SceneObject>(obj))
return sceneObject->GetParent() == parent || IsParentOf(sceneObject->GetParent(), parent);
return false;
}
SceneObject* FindPrefabObject(Actor* a, const Guid& prefabObjectId)
{
if (a->GetPrefabObjectID() == prefabObjectId)
return a;
for (auto* script : a->Scripts)
{
if (script->GetPrefabObjectID() == prefabObjectId)
return script;
}
SceneObject* result = nullptr;
for (int32 i = 0; i < a->Children.Count() && !result; i++)
result = FindPrefabObject(a->Children[i], prefabObjectId);
return result;
}
void SetupObjectSpawnGroupItem(ScriptingObject* obj, Array<SpawnGroup, InlinedAllocation<8>>& spawnGroups, SpawnItem& spawnItem)
{
// Check if can fit this object into any of the existing groups (eg. script which can be spawned with parent actor)
SpawnGroup* group = nullptr;
for (auto& g : spawnGroups)
{
ScriptingObject* groupRoot = g.Items[0]->Object.Get();
if (IsParentOf(obj, groupRoot))
{
// Reuse existing group (append)
g.Items.Add(&spawnItem);
group = &g;
break;
}
}
if (group)
return;
// Check if can override any of the existing groups (eg. actor which should be spawned before scripts)
for (auto& g : spawnGroups)
{
ScriptingObject* groupRoot = g.Items[0]->Object.Get();
if (IsParentOf(groupRoot, obj))
{
// Reuse existing group (as a root)
g.Items.Insert(0, &spawnItem);
group = &g;
break;
}
}
if (group)
return;
// Create new group
group = &spawnGroups.AddOne();
group->Items.Add(&spawnItem);
}
void FindObjectsForSpawn(SpawnGroup& group, ChunkedArray<SpawnItem, 256>& spawnItems, ScriptingObject* obj)
{
// Add any registered network objects
auto it = Objects.Find(obj->GetID());
if (it != Objects.End())
{
auto& item = it->Item;
if (!item.Spawned)
{
// One of the parents of this object is being spawned so spawn it too
item.Spawned = true;
auto& spawnItem = spawnItems.AddOne();
spawnItem.Object = obj;
spawnItem.Targets.Link(item.TargetClientIds);
spawnItem.OwnerClientId = item.OwnerClientId;
spawnItem.Role = item.Role;
group.Items.Add(&spawnItem);
}
}
// Iterate over children
if (auto* actor = ScriptingObject::Cast<Actor>(obj))
{
for (auto* script : actor->Scripts)
FindObjectsForSpawn(group, spawnItems, script);
for (auto* child : actor->Children)
FindObjectsForSpawn(group, spawnItems, child);
}
}
FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj)
{
item.Dirty();
if (Hierarchy)
Hierarchy->DirtyObject(obj);
}
PartsItem* AddPartsItem(Array<PartsItem>& items, NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
{
// Reuse or add part item
PartsItem* item = nullptr;
for (auto& e : items)
{
if (e.OwnerFrame == ownerFrame && e.Data.Count() == dataSize && e.ObjectId == objectId)
{
// Reuse
item = &e;
break;
}
}
if (!item)
{
// Add
item = &items.AddOne();
item->ObjectId = objectId;
item->PartsLeft = partsCount;
item->OwnerFrame = ownerFrame;
item->OwnerClientId = senderClientId;
item->Data.Resize(dataSize);
}
// Copy part data
ASSERT(item->PartsLeft > 0);
item->PartsLeft--;
ASSERT(partStart + partSize <= item->Data.Count());
const void* partData = event.Message.SkipBytes(partSize);
Platform::MemoryCopy(item->Data.Get() + partStart, partData, partSize);
return item;
}
FORCE_INLINE PartsItem* AddObjectReplicateItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
{
return AddPartsItem(ReplicationParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId);
}
void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, byte* data, uint32 dataSize, uint32 senderClientId)
{
ScriptingObject* obj = item.Object.Get();
if (!obj)
return;
// Skip replication if we own the object (eg. late replication message after ownership change)
if (item.Role == NetworkObjectRole::OwnedAuthoritative)
return;
// Drop object replication if it has old data (eg. newer message was already processed due to unordered channel usage)
if (item.LastOwnerFrame >= ownerFrame)
return;
item.LastOwnerFrame = ownerFrame;
// Setup message reading stream
if (CachedReadStream == nullptr)
CachedReadStream = New<NetworkStream>();
NetworkStream* stream = CachedReadStream;
stream->Initialize(data, dataSize);
stream->SenderId = senderClientId;
// Deserialize object
Scripting::ObjectsLookupIdMapping.Set(&IdsRemappingTable);
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, false);
if (failed)
{
//NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
}
if (item.AsNetworkObject)
{
item.AsNetworkObject->OnNetworkDeserialize();
if (!item.Synced)
{
item.Synced = true;
item.AsNetworkObject->OnNetworkSync();
}
}
// Speed up replication of client-owned objects to other clients from server to reduce lag (data has to go from client to server and then to other clients)
if (NetworkManager::IsServer())
DirtyObjectImpl(item, obj);
}
FORCE_INLINE PartsItem* AddObjectRpcItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
{
return AddPartsItem(RpcParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId);
}
void InvokeObjectRpc(const NetworkRpcInfo* info, byte* data, uint32 dataSize, uint32 senderClientId, ScriptingObject* obj)
{
// Setup message reading stream
if (CachedReadStream == nullptr)
CachedReadStream = New<NetworkStream>();
NetworkStream* stream = CachedReadStream;
stream->SenderId = senderClientId;
stream->Initialize(data, dataSize);
// Execute RPC
info->Execute(obj, stream, info->Tag);
}
void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const Guid& prefabId, const NetworkMessageObjectSpawnItem* msgDataItems)
{
ScopeLock lock(ObjectsLock);
// Check if that object has been already spawned
auto& rootItem = msgDataItems[0];
NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
if (root)
{
// Object already exists locally so just synchronize the ownership (and mark as spawned)
for (int32 i = 0; i < msgData.ItemsCount; i++)
{
auto& msgDataItem = msgDataItems[i];
NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
auto& item = *e;
item.Spawned = true;
if (NetworkManager::IsClient())
{
// Server always knows the best so update ownership of the existing object
item.OwnerClientId = msgData.OwnerClientId;
item.RepCache.Clear();
if (item.Role == NetworkObjectRole::OwnedAuthoritative)
{
if (Hierarchy)
Hierarchy->AddObject(item.Object);
item.Role = NetworkObjectRole::Replicated;
}
}
else if (item.OwnerClientId != msgData.OwnerClientId)
{
// Other client spawned object with a different owner
// TODO: send reply message to inform about proper object ownership that client
}
}
return;
}
// Recreate object locally (spawn only root)
Actor* prefabInstance = nullptr;
Array<ScriptingObject*> objects;
if (prefabId.IsValid())
{
const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
Actor* parentActor = parent && parent->Object && parent->Object->Is<Actor>() ? parent->Object.As<Actor>() : nullptr;
if (parentActor && parentActor->GetPrefabID() == prefabId)
{
// Reuse parent object as prefab instance
prefabInstance = parentActor;
}
else if ((parentActor = Scripting::TryFindObject<Actor>(rootItem.ParentId)))
{
// Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
for (Actor* child : parentActor->Children)
{
if (child->GetPrefabID() == prefabId)
{
if (Objects.Contains(child->GetID()))
{
ScriptingObject* obj = FindPrefabObject(child, rootItem.PrefabObjectID);
if (Objects.Contains(obj->GetID()))
{
// Other instance with already spawned network object
obj = nullptr;
}
else
{
// Reuse already spawned object within a parent
prefabInstance = child;
break;
}
}
}
}
}
if (!prefabInstance)
{
// Spawn prefab
auto prefab = (Prefab*)LoadAsset(prefabId, Prefab::TypeInitializer);
if (!prefab)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", prefabId.ToString());
return;
}
prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
if (!prefabInstance)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", prefabId.ToString());
return;
}
}
// Resolve objects from prefab instance
objects.Resize(msgData.ItemsCount);
for (int32 i = 0; i < msgData.ItemsCount; i++)
{
auto& msgDataItem = msgDataItems[i];
ScriptingObject* obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
if (!obj)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), prefabId.ToString());
Delete(prefabInstance);
return;
}
objects[i] = obj;
}
}
else if (msgData.ItemsCount == 1)
{
// Spawn object
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
ScriptingObject* obj = ScriptingObject::NewObject(objectType);
if (!obj)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
return;
}
objects.Add(obj);
}
else
{
// Spawn objects
objects.Resize(msgData.ItemsCount);
for (int32 i = 0; i < msgData.ItemsCount; i++)
{
auto& msgDataItem = msgDataItems[i];
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgDataItem.ObjectTypeName);
ScriptingObject* obj = ScriptingObject::NewObject(objectType);
if (!obj)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgDataItem.ObjectTypeName));
for (ScriptingObject* e : objects)
Delete(e);
return;
}
objects[i] = obj;
if (i != 0)
{
// Link hierarchy of spawned objects before calling any networking code for them
if (auto sceneObject = ScriptingObject::Cast<SceneObject>(obj))
{
Actor* parent = nullptr;
for (int32 j = 0; j < i; j++)
{
if (msgDataItems[j].ObjectId == msgDataItem.ParentId)
{
parent = ScriptingObject::Cast<Actor>(objects[j]);
break;
}
}
if (parent)
sceneObject->SetParent(parent);
}
}
}
}
// Add all newly spawned objects
for (int32 i = 0; i < msgData.ItemsCount; i++)
{
auto& msgDataItem = msgDataItems[i];
ScriptingObject* obj = objects[i];
if (!obj->IsRegistered())
obj->RegisterObject();
const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
// Add object to the list
NetworkReplicatedObject item;
item.Object = obj;
item.AsNetworkObject = ScriptingObject::ToInterface<INetworkObject>(obj);
item.ObjectId = obj->GetID();
item.ParentId = parent ? parent->ObjectId : Guid::Empty;
item.OwnerClientId = msgData.OwnerClientId;
item.Role = NetworkObjectRole::Replicated;
if (item.OwnerClientId == NetworkManager::LocalClientId)
{
// Upgrade ownership automatically (eg. server spawned object that local client should own)
item.Role = NetworkObjectRole::OwnedAuthoritative;
}
item.Spawned = true;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
Objects.Add(MoveTemp(item));
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->AddObject(obj);
// Boost future lookups by using indirection
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
}
// Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object)
for (int32 i = 0; i < msgData.ItemsCount; i++)
{
auto& msgDataItem = msgDataItems[i];
ScriptingObject* obj = objects[i];
auto it = Objects.Find(obj->GetID());
auto& item = it->Item;
const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
// Automatic parenting for scene objects
auto sceneObject = ScriptingObject::Cast<SceneObject>(obj);
if (sceneObject)
{
if (parent && parent->Object.Get() && parent->Object->Is<Actor>())
sceneObject->SetParent(parent->Object.As<Actor>());
else if (auto* parentActor = Scripting::TryFindObject<Actor>(msgDataItem.ParentId))
sceneObject->SetParent(parentActor);
else if (msgDataItem.ParentId.IsValid())
{
#if USE_NETWORK_REPLICATOR_LOG
// Ignore case when parent object in a message was a scene (eg. that is already unloaded on a client)
AssetInfo assetInfo;
if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName != TEXT("FlaxEngine.SceneAsset"))
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
}
#endif
}
}
else if (!parent && msgDataItem.ParentId.IsValid())
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
}
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkSpawn();
}
// TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
}
NetworkRpcParams::NetworkRpcParams(const NetworkStream* stream)
: SenderId(stream->SenderId)
{
}
#if !COMPILE_WITHOUT_CSHARP
#include "Engine/Scripting/ManagedCLR/MUtils.h"
void INetworkSerializable_Managed(void* instance, NetworkStream* stream, void* tag)
{
auto signature = (Function<void(void*, void*)>::Signature)tag;
signature(instance, stream);
}
void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, const Function<void(void*, void*)>& serialize, const Function<void(void*, void*)>& deserialize)
{
// This assumes that C# glue code passed static method pointer (via Marshal.GetFunctionPointerForDelegate)
AddSerializer(typeHandle, INetworkSerializable_Managed, INetworkSerializable_Managed, (void*)*(SerializeFunc*)&serialize, (void*)*(SerializeFunc*)&deserialize);
}
void RPC_Execute_Managed(ScriptingObject* obj, NetworkStream* stream, void* tag)
{
auto signature = (Function<void(void*, void*)>::Signature)tag;
signature(obj, stream);
}
void NetworkReplicator::AddRPC(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name, const Function<void(void*, void*)>& execute, bool isServer, bool isClient, NetworkChannelType channel)
{
if (!typeHandle)
return;
NetworkRpcInfo rpcInfo;
rpcInfo.Server = isServer;
rpcInfo.Client = isClient;
rpcInfo.Channel = (uint8)channel;
rpcInfo.Invoke = nullptr; // C# RPCs invoking happens on C# side (build-time code generation)
rpcInfo.Execute = RPC_Execute_Managed;
rpcInfo.Tag = (void*)*(SerializeFunc*)&execute;
// Add to the global RPCs table
const NetworkRpcName rpcName(typeHandle, GetCSharpCachedName(name));
NetworkRpcInfo::RPCsTable[rpcName] = rpcInfo;
}
bool NetworkReplicator::CSharpEndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, MArray* targetIds)
{
return EndInvokeRPC(obj, type, GetCSharpCachedName(name), argsStream, MUtils::ToSpan<uint32>(targetIds));
}
StringAnsiView NetworkReplicator::GetCSharpCachedName(const StringAnsiView& name)
{
// Cache method name on a heap to support C# hot-reloads (also glue code from C# passes view to the stack-only text so cache it here)
StringAnsi* result;
if (!CSharpCachedNames.TryGet(name, result))
{
result = New<StringAnsi>(name);
CSharpCachedNames.Add(StringAnsiView(*result), result);
}
return StringAnsiView(*result);
}
#endif
NetworkReplicationHierarchy* NetworkReplicator::GetHierarchy()
{
return Hierarchy;
}
void NetworkReplicator::SetHierarchy(NetworkReplicationHierarchy* value)
{
ScopeLock lock(ObjectsLock);
if (Hierarchy == value)
return;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Set hierarchy to '{}'", value ? value->ToString() : String::Empty);
if (Hierarchy)
{
// Clear old hierarchy
Delete(Hierarchy);
}
Hierarchy = value;
if (value)
{
// Add all owned objects to the hierarchy
for (auto& e : Objects)
{
if (e.Item.Object && e.Item.Role == NetworkObjectRole::OwnedAuthoritative)
value->AddObject(e.Item.Object);
}
}
}
void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, SerializeFunc serialize, SerializeFunc deserialize, void* serializeTag, void* deserializeTag)
{
if (!typeHandle)
return;
PROFILE_MEM(Networking);
const Serializer serializer{ { serialize, deserialize }, { serializeTag, deserializeTag } };
SerializersTable[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
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)
{
if (interface->IsNative)
{
// Native interface (implemented in C++)
serializer.Methods[0] = INetworkSerializable_Native_Serialize;
serializer.Methods[1] = INetworkSerializable_Native_Deserialize;
serializer.Tags[0] = serializer.Tags[1] = (void*)(intptr)interface->VTableOffset; // Pass VTableOffset to the callback
}
else
{
// Generic interface (implemented in C# or elsewhere)
ASSERT(type.Type == ScriptingTypes::Script);
serializer.Methods[0] = INetworkSerializable_Script_Serialize;
serializer.Methods[1] = INetworkSerializable_Script_Deserialize;
serializer.Tags[0] = serializer.Tags[1] = nullptr;
}
PROFILE_MEM(Networking);
SerializersTable.Add(typeHandle, serializer);
}
else if (const ScriptingTypeHandle baseTypeHandle = typeHandle.GetType().GetBaseType())
{
// Fallback to base type
return InvokeSerializer(baseTypeHandle, instance, stream, serialize);
}
else
return true;
}
// Invoke serializer
const byte idx = serialize ? 0 : 1;
serializer.Methods[idx](instance, stream, serializer.Tags[idx]);
return false;
}
void NetworkReplicator::AddObject(ScriptingObject* obj, const ScriptingObject* parent)
{
if (!obj || NetworkManager::IsOffline())
return;
PROFILE_MEM(Networking);
ScopeLock lock(ObjectsLock);
if (Objects.Contains(obj))
return;
// Automatic parenting for scene objects
if (!parent)
{
auto sceneObject = ScriptingObject::Cast<SceneObject>(obj);
if (sceneObject)
parent = sceneObject->GetParent();
}
// Ensure to register object in a scripting system (eg. lookup by ObjectId will work)
if (!obj->IsRegistered())
obj->RegisterObject();
// Add object to the list
NetworkReplicatedObject item;
item.Object = obj;
item.AsNetworkObject = ScriptingObject::ToInterface<INetworkObject>(obj);
item.ObjectId = obj->GetID();
item.ParentId = parent ? parent->GetID() : Guid::Empty;
item.OwnerClientId = NetworkManager::ServerClientId; // Server owns objects by default
item.Role = NetworkManager::IsClient() ? NetworkObjectRole::Replicated : NetworkObjectRole::OwnedAuthoritative;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->GetType().ToString() : String::Empty);
for (const SpawnItem& spawnItem : SpawnQueue)
{
if (spawnItem.HasOwnership && spawnItem.HierarchicalOwnership)
{
if (IsParentOf(obj, spawnItem.Object))
{
// Inherit ownership
item.Role = spawnItem.Role;
item.OwnerClientId = spawnItem.OwnerClientId;
break;
}
}
}
Objects.Add(MoveTemp(item));
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->AddObject(obj);
}
void NetworkReplicator::RemoveObject(ScriptingObject* obj)
{
if (!obj || NetworkManager::IsOffline())
return;
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
return;
// Remove object from the list
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", obj->GetID().ToString(), it->Item.ParentId.ToString());
if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
Objects.Remove(it);
}
void NetworkReplicator::SpawnObject(ScriptingObject* obj)
{
DataContainer<uint32> clientIds;
SpawnObject(obj, MoveTemp(clientIds));
}
void NetworkReplicator::SpawnObject(ScriptingObject* obj, const DataContainer<uint32>& clientIds)
{
if (!obj || NetworkManager::IsOffline())
return;
PROFILE_MEM(Networking);
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it != Objects.End() && it->Item.Spawned)
return; // Skip if object was already spawned
// Register for spawning (batched during update)
auto& spawn = SpawnQueue.AddOne();
spawn.Object = obj;
spawn.Targets.Copy(clientIds);
}
void NetworkReplicator::DespawnObject(ScriptingObject* obj)
{
if (!obj || NetworkManager::IsOffline())
return;
PROFILE_MEM(Networking);
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
{
// Special case if we're just spawning this object
for (int32 i = 0; i < SpawnQueue.Count(); i++)
{
auto& item = SpawnQueue[i];
if (item.Object == obj)
{
SpawnQueue.RemoveAt(i);
DeleteNetworkObject(obj);
break;
}
}
return;
}
auto& item = it->Item;
if (item.Object != obj || !item.Spawned || item.OwnerClientId != NetworkManager::LocalClientId)
return;
// Register for despawning (batched during update)
auto& despawn = DespawnQueue.AddOne();
despawn.Id = obj->GetID();
despawn.Targets = item.TargetClientIds;
// Prevent spawning
for (int32 i = 0; i < SpawnQueue.Count(); i++)
{
if (SpawnQueue[i].Object == obj)
{
SpawnQueue.RemoveAt(i);
break;
}
}
// Delete object locally
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", item.ObjectId);
DespawnedObjects.Add(item.ObjectId);
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
Objects.Remove(it);
DeleteNetworkObject(obj);
}
bool NetworkReplicator::HasObject(const ScriptingObject* obj)
{
if (obj)
{
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it != Objects.End())
return true;
for (const SpawnItem& item : SpawnQueue)
{
if (item.Object == obj)
return true;
}
}
return false;
}
void NetworkReplicator::MapObjectId(Guid& objectId)
{
if (!IdsRemappingTable.TryGet(objectId, objectId))
{
// Try inverse mapping
IdsRemappingTable.KeyOf(objectId, &objectId);
}
}
void NetworkReplicator::AddObjectIdMapping(const ScriptingObject* obj, const Guid& objectId)
{
CHECK(obj);
CHECK(objectId.IsValid());
const Guid id = obj->GetID();
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", objectId, id.ToString(), obj->GetType().ToString());
IdsRemappingTable[objectId] = id;
}
ScriptingObject* NetworkReplicator::ResolveForeignObject(Guid objectId)
{
if (const auto& object = ResolveObject(objectId))
return object->Object.Get();
return nullptr;
}
uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj)
{
uint32 id = NetworkManager::ServerClientId;
if (obj && NetworkManager::IsConnected())
{
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it != Objects.End())
id = it->Item.OwnerClientId;
else
{
for (const SpawnItem& item : SpawnQueue)
{
if (item.Object == obj)
{
if (item.HasOwnership)
id = item.OwnerClientId;
#if USE_NETWORK_REPLICATOR_LOG
return id;
#else
break;
#endif
}
}
#if USE_NETWORK_REPLICATOR_LOG
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString());
#endif
}
}
return id;
}
NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj)
{
NetworkObjectRole role = NetworkObjectRole::None;
if (obj && NetworkManager::IsConnected())
{
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it != Objects.End())
role = it->Item.Role;
else
{
for (const SpawnItem& item : SpawnQueue)
{
if (item.Object == obj)
{
if (item.HasOwnership)
role = item.Role;
#if USE_NETWORK_REPLICATOR_LOG
return role;
#else
break;
#endif
}
}
#if USE_NETWORK_REPLICATOR_LOG
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString());
#endif
}
}
return role;
}
void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerClientId, NetworkObjectRole localRole, bool hierarchical)
{
if (!obj || NetworkManager::IsOffline())
return;
const Guid objectId = obj->GetID();
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(objectId);
if (it == Objects.End())
{
// Special case if we're just spawning this object
for (int32 i = 0; i < SpawnQueue.Count(); i++)
{
auto& item = SpawnQueue[i];
if (item.Object == obj)
{
#if !BUILD_RELEASE
if (ownerClientId == NetworkManager::LocalClientId)
{
// Ensure local client owns that object actually
if (localRole != NetworkObjectRole::OwnedAuthoritative)
{
LOG(Error, "Cannot change ownership of object (Id={}) to the local client (Id={}) if the local role is not set to OwnedAuthoritative.", obj->GetID(), ownerClientId);
return;
}
}
else
{
// Ensure local client doesn't own that object since it's owned by other client
if (localRole == NetworkObjectRole::OwnedAuthoritative)
{
LOG(Error, "Cannot change ownership of object (Id={}) to the remote client (Id={}) if the local role is set to OwnedAuthoritative.", obj->GetID(), ownerClientId);
return;
}
}
#endif
item.HasOwnership = true;
item.HierarchicalOwnership = hierarchical;
item.OwnerClientId = ownerClientId;
item.Role = localRole;
break;
}
}
}
else
{
auto& item = it->Item;
if (item.Object != obj)
return;
// Check if this client is object owner
if (item.OwnerClientId == NetworkManager::LocalClientId)
{
// Check if object owner will change
if (item.OwnerClientId != ownerClientId)
{
// Change role locally
#if !BUILD_RELEASE
if (localRole == NetworkObjectRole::OwnedAuthoritative)
{
LOG(Error, "Cannot change ownership of object (Id={}) to the remote client (Id={}) if the local role is set to OwnedAuthoritative.", obj->GetID(), ownerClientId);
return;
}
#endif
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
item.OwnerClientId = ownerClientId;
item.LastOwnerFrame = 1;
item.Role = localRole;
item.RepCache.Clear();
SendObjectRoleMessage(item);
}
}
else
{
// Allow to change local role of the object (except ownership)
#if !BUILD_RELEASE
if (localRole == NetworkObjectRole::OwnedAuthoritative)
{
LOG(Error, "Cannot change ownership of object (Id={}) to the remote client (Id={}) if the local role is set to OwnedAuthoritative.", obj->GetID(), ownerClientId);
return;
}
#endif
if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
item.Role = localRole;
}
}
// Go down hierarchy
if (hierarchical)
{
for (auto& e : Objects)
{
if (e.Item.ParentId == objectId)
SetObjectOwnership(e.Item.Object.Get(), ownerClientId, localRole, hierarchical);
}
for (const SpawnItem& spawnItem : SpawnQueue)
{
if (IsParentOf(spawnItem.Object, obj))
SetObjectOwnership(spawnItem.Object, ownerClientId, localRole, hierarchical);
}
}
}
void NetworkReplicator::DirtyObject(ScriptingObject* obj)
{
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
return;
auto& item = it->Item;
if (item.Object != obj || item.Role != NetworkObjectRole::OwnedAuthoritative)
return;
DirtyObjectImpl(item, obj);
}
Dictionary<NetworkRpcName, NetworkRpcInfo> NetworkRpcInfo::RPCsTable;
NetworkStream* NetworkReplicator::BeginInvokeRPC()
{
PROFILE_MEM(Networking);
if (CachedWriteStream == nullptr)
CachedWriteStream = New<NetworkStream>();
CachedWriteStream->Initialize();
CachedWriteStream->SenderId = NetworkManager::LocalClientId;
Scripting::ObjectsLookupIdMapping.Set(&IdsRemappingTable);
return CachedWriteStream;
}
bool NetworkReplicator::EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, Span<uint32> targetIds)
{
if (targetIds.IsValid() && targetIds.Length() == 0)
return true; // Target list is provided, but it's empty so nobody will get this RPC
Scripting::ObjectsLookupIdMapping.Set(nullptr);
const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(NetworkRpcName(type, name));
if (!info || !obj || NetworkManager::IsOffline())
return false;
PROFILE_MEM(Networking);
ObjectsLock.Lock();
auto& rpc = RpcQueue.AddOne();
rpc.Object = obj;
rpc.Name.First = type;
rpc.Name.Second = name;
rpc.Info = *info;
rpc.ArgsData.Copy(Span<byte>(argsStream->GetBuffer(), argsStream->GetPosition()));
rpc.Targets.Copy(targetIds);
ObjectsLock.Unlock();
// Check if skip local execution (eg. server rpc called from client or client rpc with specific targets)
const NetworkManagerMode networkMode = NetworkManager::Mode;
if (info->Server && networkMode == NetworkManagerMode::Client)
return true;
if (info->Client && networkMode == NetworkManagerMode::Server)
return true;
if (info->Client && networkMode == NetworkManagerMode::Host && targetIds.IsValid() && !SpanContains(targetIds, NetworkManager::LocalClientId))
return true;
return false;
}
void NetworkInternal::NetworkReplicatorClientConnected(NetworkClient* client)
{
ScopeLock lock(ObjectsLock);
NewClients.Add(client);
ASSERT(sizeof(NetworkClientsMask) * 8 >= (uint32)NetworkManager::Clients.Count()); // Ensure that clients mask can hold all of clients
}
void NetworkInternal::NetworkReplicatorClientDisconnected(NetworkClient* client)
{
ScopeLock lock(ObjectsLock);
NewClients.Remove(client);
// Remove any objects owned by that client
const uint32 clientId = client->ClientId;
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (obj && item.Spawned && item.OwnerClientId == clientId)
{
// Register for despawning (batched during update)
auto& despawn = DespawnQueue.AddOne();
despawn.Id = obj->GetID();
despawn.Targets = MoveTemp(item.TargetClientIds);
// Delete object locally
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", item.ObjectId);
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
DeleteNetworkObject(obj);
Objects.Remove(it);
}
}
}
void NetworkInternal::NetworkReplicatorClear()
{
ScopeLock lock(ObjectsLock);
// Cleanup
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Shutdown");
NetworkReplicator::SetHierarchy(nullptr);
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (obj && item.Spawned)
{
// Cleanup any spawned objects
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
DeleteNetworkObject(obj);
Objects.Remove(it);
}
}
Objects.Clear();
RpcQueue.Clear();
SpawnQueue.Clear();
DespawnQueue.Clear();
IdsRemappingTable.Clear();
SAFE_DELETE(CachedWriteStream);
SAFE_DELETE(CachedReadStream);
SAFE_DELETE(CachedReplicationResult);
NewClients.Clear();
CachedTargets.Clear();
DespawnedObjects.Clear();
}
void NetworkInternal::NetworkReplicatorPreUpdate()
{
// Inject ObjectsLookupIdMapping to properly map networked object ids into local object ids (deserialization with Scripting::TryFindObject will remap objects)
Scripting::ObjectsLookupIdMapping.Set(&IdsRemappingTable);
}
void NetworkInternal::NetworkReplicatorUpdate()
{
PROFILE_CPU();
ScopeLock lock(ObjectsLock);
if (Objects.Count() == 0)
return;
const bool isClient = NetworkManager::IsClient();
if (!isClient && NewClients.Count() != 0)
{
// Sync any previously spawned objects with late-joining clients
PROFILE_CPU_NAMED("NewClients");
// TODO: try iterative loop over several frames to reduce both server and client perf-spikes in case of large amount of spawned objects
ChunkedArray<SpawnItem, 256> spawnItems;
Array<SpawnGroup, InlinedAllocation<8>> spawnGroups;
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (!obj || !item.Spawned)
continue;
// Setup spawn item for this object
auto& spawnItem = spawnItems.AddOne();
spawnItem.Object = obj;
spawnItem.Targets.Link(item.TargetClientIds);
spawnItem.OwnerClientId = item.OwnerClientId;
spawnItem.Role = item.Role;
SetupObjectSpawnGroupItem(obj, spawnGroups, spawnItem);
}
// Groups of objects to spawn
for (SpawnGroup& g : spawnGroups)
{
SendObjectSpawnMessage(g, NewClients);
}
NewClients.Clear();
}
// Despawn
if (DespawnQueue.Count() != 0)
{
PROFILE_CPU_NAMED("DespawnQueue");
for (DespawnItem& e : DespawnQueue)
{
SendDespawn(e);
}
DespawnQueue.Clear();
}
// Spawn
if (SpawnQueue.Count() != 0)
{
PROFILE_CPU_NAMED("SpawnQueue");
// Propagate hierarchical ownership from spawned parent to spawned child objects (eg. spawned script and spawned actor with set hierarchical ownership on actor which should affect script too)
// TODO: maybe we can propagate ownership within spawn groups only?
for (SpawnItem& e : SpawnQueue)
{
if (e.HasOwnership && e.HierarchicalOwnership)
{
for (auto& q : SpawnQueue)
{
if (!q.HasOwnership && IsParentOf(q.Object, e.Object))
{
// Inherit ownership
q.HasOwnership = true;
q.Role = e.Role;
q.OwnerClientId = e.OwnerClientId;
break;
}
}
}
}
// Batch spawned objects into groups (eg. player actor with scripts and child actors merged as a single spawn message)
// That's because NetworkReplicator::SpawnObject can be called in separate for different actors/scripts of a single prefab instance but we want to spawn it at once over the network
Array<SpawnGroup, InlinedAllocation<8>> spawnGroups;
for (SpawnItem& e : SpawnQueue)
{
ScriptingObject* obj = e.Object.Get();
if (!obj)
continue;
auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
{
// Ensure that object is added to the replication locally
NetworkReplicator::AddObject(obj);
it = Objects.Find(obj->GetID());
}
if (it == Objects.End())
continue; // Skip deleted objects
auto& item = it->Item;
if (item.OwnerClientId != NetworkManager::LocalClientId || item.Role != NetworkObjectRole::OwnedAuthoritative)
continue; // Skip spawning objects that we don't own
if (e.HasOwnership)
{
if (item.Role != e.Role)
{
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
item.Role = e.Role;
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->AddObject(obj);
}
item.OwnerClientId = e.OwnerClientId;
if (e.HierarchicalOwnership)
NetworkReplicator::SetObjectOwnership(obj, e.OwnerClientId, e.Role, true);
}
if (e.Targets.IsValid())
{
// TODO: if we spawn object with custom set of targets clientsIds on client, then send it over to the server
if (NetworkManager::IsClient())
MISSING_CODE("Sending TargetClientIds over to server for partial object replication.");
item.TargetClientIds = MoveTemp(e.Targets);
}
item.Spawned = true;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
SetupObjectSpawnGroupItem(obj, spawnGroups, e);
}
// Spawn groups of objects
ChunkedArray<SpawnItem, 256> spawnItems;
for (SpawnGroup& g : spawnGroups)
{
// Include any added objects within spawn group that were not spawned manually (eg. AddObject for script/actor attached to spawned actor)
ScriptingObject* groupRoot = g.Items[0]->Object.Get();
FindObjectsForSpawn(g, spawnItems, groupRoot);
SendObjectSpawnMessage(g, NetworkManager::Clients);
spawnItems.Clear();
}
SpawnQueue.Clear();
}
// Apply parts replication
{
PROFILE_CPU_NAMED("ReplicationParts");
for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--)
{
auto& e = ReplicationParts[i];
if (e.PartsLeft > 0)
{
// TODO: remove replication items after some TTL to reduce memory usage
continue;
}
ScriptingObject* obj = e.Object.Get();
if (obj)
{
auto it = Objects.Find(obj->GetID());
if (it != Objects.End())
{
auto& item = it->Item;
// Replicate from all collected parts data
InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId);
}
}
ReplicationParts.RemoveAt(i);
}
}
// TODO: remove items from RpcParts after some TTL to reduce memory usage
// TODO: remove items from SpawnParts after some TTL to reduce memory usage
// Replicate all owned networked objects with other clients or server
if (!CachedReplicationResult)
CachedReplicationResult = New<NetworkReplicationHierarchyUpdateResult>();
CachedReplicationResult->Init();
if ((!isClient && NetworkManager::Clients.IsEmpty()) || NetworkManager::NetworkFPS < -ZeroTolerance)
{
// No need to update replication when nobody's around or when replication is disabled
}
else if (Hierarchy)
{
// Tick using hierarchy
PROFILE_CPU_NAMED("ReplicationHierarchyUpdate");
Hierarchy->Update(CachedReplicationResult);
}
else
{
// Tick all owned objects
PROFILE_CPU_NAMED("ReplicationUpdate");
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (!obj)
{
// Object got deleted
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
Objects.Remove(it);
continue;
}
if (item.Role != NetworkObjectRole::OwnedAuthoritative)
continue; // Send replication messages of only owned objects or from other client objects
CachedReplicationResult->AddObject(obj);
}
}
if (CachedReplicationResult->_entries.HasItems())
{
PROFILE_CPU_NAMED("Replication");
if (CachedWriteStream == nullptr)
CachedWriteStream = New<NetworkStream>();
CachedWriteStream->SenderId = NetworkManager::LocalClientId;
// TODO: use Job System when replicated objects count is large
for (auto& e : CachedReplicationResult->_entries)
{
SendReplication(e.Object, e.TargetClients);
}
}
// Invoke RPCs
{
PROFILE_CPU_NAMED("Rpc");
for (auto& e : RpcQueue)
{
SendRpc(e);
}
RpcQueue.Clear();
}
// Clear networked objects mapping table
Scripting::ObjectsLookupIdMapping.Set(nullptr);
}
void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectReplicate msgData;
NetworkMessageObjectPartPayload msgDataPayload;
Guid objectId, parentId;
StringAnsiView objectTypeName;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
event.Message.ReadNetworkId(parentId);
event.Message.ReadNetworkName(objectTypeName);
event.Message.ReadStructure(msgDataPayload);
ScopeLock lock(ObjectsLock);
if (DespawnedObjects.Contains(objectId))
return; // Skip replicating non-existing objects
NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName);
if (!e)
return;
auto& item = *e;
// Reject event from someone who is not an object owner
if (client && item.OwnerClientId != client->ClientId)
return;
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
if (msgDataPayload.PartsCount == 1)
{
// Replicate
InvokeObjectReplication(item, msgData.OwnerFrame, event.Message.Buffer + event.Message.Position, msgDataPayload.DataSize, senderClientId);
}
else
{
// Add to replication from multiple parts
PartsItem* replicateItem = AddObjectReplicateItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId);
replicateItem->Object = e->Object;
}
}
void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectPart msgData;
Guid objectId;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
ScopeLock lock(ObjectsLock);
if (DespawnedObjects.Contains(objectId))
return; // Skip replicating non-existing objects
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
AddObjectReplicateItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId);
}
void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectSpawn msgData;
Guid prefabId;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(prefabId);
if (msgData.ItemsCount == 0)
return;
if (msgData.UseParts)
{
// Allocate spawn message parts collecting
auto& parts = SpawnParts.AddOne();
parts.MsgData = msgData;
parts.PrefabId = prefabId;
parts.Items.Resize(msgData.ItemsCount);
for (auto& item : parts.Items)
item.ObjectId = Guid::Empty; // Mark as not yet received
}
else
{
const auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
InvokeObjectSpawn(msgData, prefabId, msgDataItems);
}
}
void NetworkInternal::OnNetworkMessageObjectSpawnPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectSpawnPart msgData;
event.Message.ReadStructure(msgData);
int32 spawnPartsIndex;
for (spawnPartsIndex = 0; spawnPartsIndex < SpawnParts.Count(); spawnPartsIndex++)
{
// Find spawn parts container that matches this spawn message (unique pair of sender and id assigned by sender)
const auto& e = SpawnParts.Get()[spawnPartsIndex];
if (e.MsgData.OwnerClientId == msgData.OwnerClientId && e.MsgData.OwnerSpawnId == msgData.OwnerSpawnId)
break;
}
if (spawnPartsIndex >= SpawnParts.Count())
{
// Invalid part or data, ignore it
return;
}
auto& spawnParts = SpawnParts.Get()[spawnPartsIndex];
// Read all items from this part
constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
while (event.Message.Position + spawnItemMaxSize <= event.Message.BufferSize)
{
const uint16 itemIndex = event.Message.ReadUInt16();
event.Message.ReadStructure(spawnParts.Items[itemIndex]);
}
// Invoke spawning if we've got all items
for (auto& e : spawnParts.Items)
{
if (!e.ObjectId.IsValid())
return;
}
InvokeObjectSpawn(spawnParts.MsgData, spawnParts.PrefabId, spawnParts.Items.Get());
SpawnParts.RemoveAt(spawnPartsIndex);
}
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectDespawn msgData;
Guid objectId;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
ScopeLock lock(ObjectsLock);
NetworkReplicatedObject* e = ResolveObject(objectId);
if (e)
{
auto& item = *e;
ScriptingObject* obj = item.Object.Get();
if (!obj || !item.Spawned)
return;
// Reject event from someone who is not an object owner
if (client && item.OwnerClientId != client->ClientId)
return;
// Remove object
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", objectId);
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
Hierarchy->RemoveObject(obj);
DespawnedObjects.Add(objectId);
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
Objects.Remove(obj);
DeleteNetworkObject(obj);
}
else
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to despawn object {}", objectId);
}
}
void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectRole msgData;
Guid objectId;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
ScopeLock lock(ObjectsLock);
NetworkReplicatedObject* e = ResolveObject(objectId);
if (e)
{
auto& item = *e;
ScriptingObject* obj = item.Object.Get();
if (!obj)
return;
// Reject event from someone who is not an object owner
if (client && item.OwnerClientId != client->ClientId)
return;
// Update
item.OwnerClientId = msgData.OwnerClientId;
item.LastOwnerFrame = 1;
if (item.OwnerClientId == NetworkManager::LocalClientId)
{
// Upgrade ownership automatically
if (Hierarchy && item.Role != NetworkObjectRole::OwnedAuthoritative)
Hierarchy->AddObject(obj);
item.Role = NetworkObjectRole::OwnedAuthoritative;
item.LastOwnerFrame = 0;
}
else if (item.Role == NetworkObjectRole::OwnedAuthoritative)
{
// Downgrade ownership automatically
if (Hierarchy)
Hierarchy->RemoveObject(obj);
item.Role = NetworkObjectRole::Replicated;
}
if (!NetworkManager::IsClient())
{
// Server has to broadcast ownership message to the other clients
SendObjectRoleMessage(item, client);
}
}
else
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object role update {}", objectId);
}
}
void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectRpc msgData;
NetworkMessageObjectPartPayload msgDataPayload;
Guid objectId, parentId;
StringAnsiView objectTypeName, rpcTypeName, rpcName;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
event.Message.ReadNetworkId(parentId);
event.Message.ReadNetworkName(objectTypeName);
event.Message.ReadNetworkName(rpcTypeName);
event.Message.ReadNetworkName(rpcName);
event.Message.ReadStructure(msgDataPayload);
ScopeLock lock(ObjectsLock);
// Find RPC info
NetworkRpcName name;
name.First = Scripting::FindScriptingType(rpcTypeName);
name.Second = rpcName;
const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name);
if (!info)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(rpcTypeName), String(rpcName), objectId);
return;
}
NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName);
if (e)
{
auto& item = *e;
ScriptingObject* obj = item.Object.Get();
if (!obj)
return;
// Validate RPC
if (info->Server && NetworkManager::IsClient())
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot invoke server RPC {}::{} on client", String(rpcTypeName), String(rpcName));
return;
}
if (info->Client && NetworkManager::IsServer())
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot invoke client RPC {}::{} on server", String(rpcTypeName), String(rpcName));
return;
}
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
if (msgDataPayload.PartsCount == 1)
{
// Call RPC
InvokeObjectRpc(info, event.Message.Buffer + event.Message.Position, msgDataPayload.DataSize, senderClientId, obj);
}
else
{
// Add to RPC from multiple parts
PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId);
rpcItem->Object = e->Object;
rpcItem->Tag = info;
}
}
else if (info->Channel != static_cast<uint8>(NetworkChannelType::Unreliable) && info->Channel != static_cast<uint8>(NetworkChannelType::UnreliableOrdered))
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", objectId, String(rpcTypeName), String(rpcName));
}
}
void NetworkInternal::OnNetworkMessageObjectRpcPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
PROFILE_CPU();
NetworkMessageObjectPart msgData;
Guid objectId;
event.Message.ReadStructure(msgData);
event.Message.ReadNetworkId(objectId);
ScopeLock lock(ObjectsLock);
if (DespawnedObjects.Contains(objectId))
return; // Skip replicating non-existing objects
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId);
if (rpcItem && rpcItem->PartsLeft == 0)
{
// Got all parts so invoke RPC
ScriptingObject* obj = rpcItem->Object.Get();
if (obj)
{
InvokeObjectRpc((const NetworkRpcInfo*)rpcItem->Tag, rpcItem->Data.Get(), rpcItem->Data.Count(), rpcItem->OwnerClientId, obj);
}
// Remove item
int32 partIndex = (int32)((RpcParts.Get() - rpcItem) / sizeof(rpcItem));
RpcParts.RemoveAt(partIndex);
}
}