script and actors not having connect transform in on awake and in on start join auto anchor having wrong anchor because was including scale fixed selection do to changes to transform accessibility ?
2022 lines
73 KiB
C++
2022 lines
73 KiB
C++
// Copyright (c) 2012-2023 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/Scripting/Script.h"
|
|
#include "Engine/Scripting/Scripting.h"
|
|
#include "Engine/Scripting/ScriptingObjectReference.h"
|
|
#include "Engine/Threading/Threading.h"
|
|
#include "Engine/Threading/ThreadLocal.h"
|
|
|
|
#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
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectReplicate
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicate;
|
|
uint32 OwnerFrame;
|
|
Guid ObjectId; // TODO: introduce networked-ids to synchronize unique ids as ushort (less data over network)
|
|
Guid ParentId;
|
|
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
|
uint16 DataSize;
|
|
uint16 PartsCount;
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectReplicatePart
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicatePart;
|
|
uint32 OwnerFrame;
|
|
uint16 DataSize;
|
|
uint16 PartsCount;
|
|
uint16 PartStart;
|
|
uint16 PartSize;
|
|
Guid ObjectId; // TODO: introduce networked-ids to synchronize unique ids as ushort (less data over network)
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectSpawn
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
|
|
uint32 OwnerClientId;
|
|
Guid PrefabId;
|
|
uint16 ItemsCount;
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectSpawnItem
|
|
{
|
|
Guid ObjectId;
|
|
Guid ParentId;
|
|
Guid PrefabObjectID;
|
|
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectDespawn
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectDespawn;
|
|
Guid ObjectId;
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectRole
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectRole;
|
|
Guid ObjectId;
|
|
uint32 OwnerClientId;
|
|
});
|
|
|
|
PACK_STRUCT(struct NetworkMessageObjectRpc
|
|
{
|
|
NetworkMessageIDs ID = NetworkMessageIDs::ObjectRpc;
|
|
Guid ObjectId;
|
|
Guid ParentId;
|
|
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
|
char RpcTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
|
char RpcName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
|
uint16 ArgsSize;
|
|
});
|
|
|
|
struct NetworkReplicatedObject
|
|
{
|
|
ScriptingObjectReference<ScriptingObject> Object;
|
|
Guid ObjectId;
|
|
Guid ParentId;
|
|
uint32 OwnerClientId;
|
|
uint32 LastOwnerFrame = 0;
|
|
NetworkObjectRole Role;
|
|
uint8 Spawned : 1;
|
|
uint8 Synced : 1;
|
|
DataContainer<uint32> TargetClientIds;
|
|
INetworkObject* AsNetworkObject;
|
|
|
|
NetworkReplicatedObject()
|
|
{
|
|
Spawned = 0;
|
|
Synced = 0;
|
|
}
|
|
|
|
bool operator==(const NetworkReplicatedObject& other) const
|
|
{
|
|
return Object == other.Object;
|
|
}
|
|
|
|
bool operator==(const ScriptingObject* other) const
|
|
{
|
|
return Object == other;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
struct Serializer
|
|
{
|
|
NetworkReplicator::SerializeFunc Methods[2];
|
|
void* Tags[2];
|
|
};
|
|
|
|
struct ReplicateItem
|
|
{
|
|
ScriptingObjectReference<ScriptingObject> Object;
|
|
Guid ObjectId;
|
|
uint16 PartsLeft;
|
|
uint32 OwnerFrame;
|
|
uint32 OwnerClientId;
|
|
Array<byte> Data;
|
|
};
|
|
|
|
struct SpawnItem
|
|
{
|
|
ScriptingObjectReference<ScriptingObject> Object;
|
|
DataContainer<uint32> Targets;
|
|
bool HasOwnership = false;
|
|
bool HierarchicalOwnership = false;
|
|
uint32 OwnerClientId;
|
|
NetworkObjectRole Role;
|
|
};
|
|
|
|
struct SpawnGroup
|
|
{
|
|
Array<SpawnItem*, InlinedAllocation<8>> Items;
|
|
};
|
|
|
|
struct DespawnItem
|
|
{
|
|
Guid Id;
|
|
DataContainer<uint32> Targets;
|
|
};
|
|
|
|
struct RpcItem
|
|
{
|
|
ScriptingObjectReference<ScriptingObject> Object;
|
|
NetworkRpcName Name;
|
|
NetworkRpcInfo Info;
|
|
BytesContainer ArgsData;
|
|
DataContainer<uint32> Targets;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
CriticalSection ObjectsLock;
|
|
HashSet<NetworkReplicatedObject> Objects;
|
|
Array<ReplicateItem> ReplicationParts;
|
|
Array<SpawnItem> SpawnQueue;
|
|
Array<DespawnItem> DespawnQueue;
|
|
Array<RpcItem> 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;
|
|
}
|
|
|
|
class NetworkReplicationService : public EngineService
|
|
{
|
|
public:
|
|
NetworkReplicationService()
|
|
: EngineService(TEXT("Network Replication"), 1100)
|
|
{
|
|
}
|
|
|
|
void Dispose() override;
|
|
};
|
|
|
|
void NetworkReplicationService::Dispose()
|
|
{
|
|
NetworkInternal::NetworkReplicatorClear();
|
|
#if !COMPILE_WITHOUT_CSHARP
|
|
CSharpCachedNames.ClearDelete();
|
|
#endif
|
|
}
|
|
|
|
NetworkReplicationService NetworkReplicationServiceInstance;
|
|
|
|
void INetworkSerializable_Serialize(void* instance, NetworkStream* stream, void* tag)
|
|
{
|
|
const int16 vtableOffset = (int16)(intptr)tag;
|
|
((INetworkSerializable*)((byte*)instance + vtableOffset))->Serialize(stream);
|
|
}
|
|
|
|
void INetworkSerializable_Deserialize(void* instance, NetworkStream* stream, void* tag)
|
|
{
|
|
const int16 vtableOffset = (int16)(intptr)tag;
|
|
((INetworkSerializable*)((byte*)instance + vtableOffset))->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, char objectTypeName[128])
|
|
{
|
|
// 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(StringAnsiView(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);
|
|
}
|
|
|
|
FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
|
|
{
|
|
Platform::MemoryCopy(buffer, name.Get(), name.Length());
|
|
buffer[name.Length()] = 0;
|
|
}
|
|
|
|
void SendObjectSpawnMessage(const SpawnGroup& group, const Array<NetworkClient*>& clients)
|
|
{
|
|
const bool isClient = NetworkManager::IsClient();
|
|
auto* peer = NetworkManager::Peer;
|
|
NetworkMessage msg = peer->BeginSendMessage();
|
|
NetworkMessageObjectSpawn msgData;
|
|
msgData.ItemsCount = group.Items.Count();
|
|
{
|
|
// 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);
|
|
msgData.PrefabId = objScene && objScene->HasPrefabLink() ? objScene->GetPrefabID() : Guid::Empty;
|
|
|
|
// Setup clients that should receive this spawn message
|
|
auto it = Objects.Find(obj->GetID());
|
|
auto& item = it->Item;
|
|
BuildCachedTargets(clients, item.TargetClientIds);
|
|
}
|
|
msg.WriteStructure(msgData);
|
|
for (SpawnItem* e : group.Items)
|
|
{
|
|
ScriptingObject* obj = e->Object.Get();
|
|
auto it = Objects.Find(obj->GetID());
|
|
auto& item = it->Item;
|
|
|
|
// Add object into spawn message
|
|
NetworkMessageObjectSpawnItem msgDataItem;
|
|
msgDataItem.ObjectId = item.ObjectId;
|
|
msgDataItem.ParentId = item.ParentId;
|
|
if (isClient)
|
|
{
|
|
// 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();
|
|
GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
|
|
msg.WriteStructure(msgDataItem);
|
|
}
|
|
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;
|
|
msgData.ObjectId = item.ObjectId;
|
|
msgData.OwnerClientId = item.OwnerClientId;
|
|
auto peer = NetworkManager::Peer;
|
|
NetworkMessage msg = peer->BeginSendMessage();
|
|
msg.WriteStructure(msgData);
|
|
if (NetworkManager::IsClient())
|
|
{
|
|
NetworkManager::Peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
|
}
|
|
else
|
|
{
|
|
BuildCachedTargets(NetworkManager::Clients, excludedClient);
|
|
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (Hierarchy)
|
|
Hierarchy->DirtyObject(obj);
|
|
}
|
|
|
|
template<typename MessageType>
|
|
ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, const MessageType& msgData, uint16 partStart, uint16 partSize, uint32 senderClientId)
|
|
{
|
|
// Reuse or add part item
|
|
ReplicateItem* replicateItem = nullptr;
|
|
for (auto& e : ReplicationParts)
|
|
{
|
|
if (e.OwnerFrame == msgData.OwnerFrame && e.Data.Count() == msgData.DataSize && e.ObjectId == msgData.ObjectId)
|
|
{
|
|
// Reuse
|
|
replicateItem = &e;
|
|
break;
|
|
}
|
|
}
|
|
if (!replicateItem)
|
|
{
|
|
// Add
|
|
replicateItem = &ReplicationParts.AddOne();
|
|
replicateItem->ObjectId = msgData.ObjectId;
|
|
replicateItem->PartsLeft = msgData.PartsCount;
|
|
replicateItem->OwnerFrame = msgData.OwnerFrame;
|
|
replicateItem->OwnerClientId = senderClientId;
|
|
replicateItem->Data.Resize(msgData.DataSize);
|
|
}
|
|
|
|
// Copy part data
|
|
ASSERT(replicateItem->PartsLeft > 0);
|
|
replicateItem->PartsLeft--;
|
|
ASSERT(partStart + partSize <= replicateItem->Data.Count());
|
|
const void* partData = event.Message.SkipBytes(partSize);
|
|
Platform::MemoryCopy(replicateItem->Data.Get() + partStart, partData, partSize);
|
|
|
|
return replicateItem;
|
|
}
|
|
|
|
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
|
|
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);
|
|
}
|
|
|
|
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;
|
|
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)
|
|
{
|
|
serializer.Methods[0] = INetworkSerializable_Serialize;
|
|
serializer.Methods[1] = INetworkSerializable_Deserialize;
|
|
serializer.Tags[0] = serializer.Tags[1] = (void*)(intptr)interface->VTableOffset; // Pass VTableOffset to the callback
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
}
|
|
|
|
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
|
|
CHECK(localRole == NetworkObjectRole::OwnedAuthoritative);
|
|
}
|
|
else
|
|
{
|
|
// Ensure local client doesn't own that object since it's owned by other client
|
|
CHECK(localRole != NetworkObjectRole::OwnedAuthoritative);
|
|
}
|
|
#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
|
|
CHECK(localRole != NetworkObjectRole::OwnedAuthoritative);
|
|
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
|
|
Hierarchy->RemoveObject(obj);
|
|
item.OwnerClientId = ownerClientId;
|
|
item.LastOwnerFrame = 1;
|
|
item.Role = localRole;
|
|
SendObjectRoleMessage(item);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Allow to change local role of the object (except ownership)
|
|
CHECK(localRole != NetworkObjectRole::OwnedAuthoritative);
|
|
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()
|
|
{
|
|
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)
|
|
{
|
|
Scripting::ObjectsLookupIdMapping.Set(nullptr);
|
|
const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(NetworkRpcName(type, name));
|
|
if (!info || !obj || NetworkManager::IsOffline())
|
|
return false;
|
|
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();
|
|
const bool isServer = NetworkManager::IsServer();
|
|
const bool isHost = NetworkManager::IsHost();
|
|
NetworkPeer* peer = NetworkManager::Peer;
|
|
|
|
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)
|
|
{
|
|
// Send despawn message
|
|
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString());
|
|
NetworkMessageObjectDespawn msgData;
|
|
msgData.ObjectId = e.Id;
|
|
if (isClient)
|
|
{
|
|
// Remap local client object ids into server ids
|
|
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
|
|
}
|
|
NetworkMessage msg = peer->BeginSendMessage();
|
|
msg.WriteStructure(msgData);
|
|
BuildCachedTargets(NetworkManager::Clients, e.Targets);
|
|
if (isClient)
|
|
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
|
else
|
|
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
|
}
|
|
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 prevent memory leaks
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Replicate all owned networked objects with other clients or server
|
|
if (!CachedReplicationResult)
|
|
CachedReplicationResult = New<NetworkReplicationHierarchyUpdateResult>();
|
|
CachedReplicationResult->Init();
|
|
if (!isClient && NetworkManager::Clients.IsEmpty())
|
|
{
|
|
// No need to update replication when nobody's around
|
|
}
|
|
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>();
|
|
NetworkStream* stream = CachedWriteStream;
|
|
stream->SenderId = NetworkManager::LocalClientId;
|
|
// TODO: use Job System when replicated objects count is large
|
|
for (auto& e : CachedReplicationResult->_entries)
|
|
{
|
|
ScriptingObject* obj = e.Object;
|
|
auto it = Objects.Find(obj->GetID());
|
|
if (it.IsEnd())
|
|
continue;
|
|
auto& item = it->Item;
|
|
|
|
// Skip serialization of objects that none will receive
|
|
if (!isClient)
|
|
{
|
|
BuildCachedTargets(item, e.TargetClients);
|
|
if (CachedTargets.Count() == 0)
|
|
continue;
|
|
}
|
|
|
|
if (item.AsNetworkObject)
|
|
item.AsNetworkObject->OnNetworkSerialize();
|
|
|
|
// Serialize object
|
|
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());
|
|
continue;
|
|
}
|
|
|
|
// Send object to clients
|
|
const uint32 size = stream->GetPosition();
|
|
ASSERT(size <= MAX_uint16)
|
|
NetworkMessageObjectReplicate msgData;
|
|
msgData.OwnerFrame = NetworkManager::Frame;
|
|
msgData.ObjectId = item.ObjectId;
|
|
msgData.ParentId = item.ParentId;
|
|
if (isClient)
|
|
{
|
|
// Remap local client object ids into server ids
|
|
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
|
|
IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
|
|
}
|
|
GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname);
|
|
msgData.DataSize = size;
|
|
const uint32 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate);
|
|
const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart);
|
|
uint32 partsCount = 1;
|
|
uint32 dataStart = 0;
|
|
uint32 msgDataSize = size;
|
|
if (size > msgMaxData)
|
|
{
|
|
// Send msgMaxData within first message
|
|
msgDataSize = msgMaxData;
|
|
dataStart += msgMaxData;
|
|
|
|
// Send rest of the data in separate parts
|
|
partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData);
|
|
}
|
|
else
|
|
dataStart += size;
|
|
ASSERT(partsCount <= MAX_uint8)
|
|
msgData.PartsCount = partsCount;
|
|
NetworkMessage msg = peer->BeginSendMessage();
|
|
msg.WriteStructure(msgData);
|
|
msg.WriteBytes(stream->GetBuffer(), msgDataSize);
|
|
if (isClient)
|
|
peer->EndSendMessage(NetworkChannelType::Unreliable, msg);
|
|
else
|
|
peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets);
|
|
|
|
// Send all other parts
|
|
for (uint32 partIndex = 1; partIndex < partsCount; partIndex++)
|
|
{
|
|
NetworkMessageObjectReplicatePart msgDataPart;
|
|
msgDataPart.OwnerFrame = msgData.OwnerFrame;
|
|
msgDataPart.ObjectId = msgData.ObjectId;
|
|
msgDataPart.DataSize = msgData.DataSize;
|
|
msgDataPart.PartsCount = msgData.PartsCount;
|
|
msgDataPart.PartStart = dataStart;
|
|
msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData);
|
|
msg = peer->BeginSendMessage();
|
|
msg.WriteStructure(msgDataPart);
|
|
msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize);
|
|
dataStart += msgDataPart.PartSize;
|
|
if (isClient)
|
|
peer->EndSendMessage(NetworkChannelType::Unreliable, msg);
|
|
else
|
|
peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets);
|
|
}
|
|
ASSERT_LOW_LAYER(dataStart == size);
|
|
|
|
// TODO: stats for bytes send per object type
|
|
}
|
|
}
|
|
|
|
// Invoke RPCs
|
|
{
|
|
PROFILE_CPU_NAMED("Rpc");
|
|
for (auto& e : RpcQueue)
|
|
{
|
|
ScriptingObject* obj = e.Object.Get();
|
|
if (!obj)
|
|
continue;
|
|
auto it = Objects.Find(obj->GetID());
|
|
if (it == Objects.End())
|
|
{
|
|
#if USE_EDITOR || !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(), String(e.Name.Second), obj->GetID());
|
|
#endif
|
|
continue;
|
|
}
|
|
auto& item = it->Item;
|
|
|
|
// Send RPC message
|
|
//NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString());
|
|
NetworkMessageObjectRpc msgData;
|
|
msgData.ObjectId = item.ObjectId;
|
|
msgData.ParentId = item.ParentId;
|
|
if (isClient)
|
|
{
|
|
// Remap local client object ids into server ids
|
|
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
|
|
IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
|
|
}
|
|
GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname);
|
|
GetNetworkName(msgData.RpcTypeName, e.Name.First.GetType().Fullname);
|
|
GetNetworkName(msgData.RpcName, e.Name.Second);
|
|
msgData.ArgsSize = (uint16)e.ArgsData.Length();
|
|
NetworkMessage msg = peer->BeginSendMessage();
|
|
msg.WriteStructure(msgData);
|
|
msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length());
|
|
NetworkChannelType channel = (NetworkChannelType)e.Info.Channel;
|
|
if (e.Info.Server && isClient)
|
|
{
|
|
// 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
|
|
peer->EndSendMessage(channel, msg);
|
|
}
|
|
else if (e.Info.Client && (isServer || isHost))
|
|
{
|
|
// Server -> Client(s)
|
|
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId);
|
|
peer->EndSendMessage(channel, msg, CachedTargets);
|
|
}
|
|
}
|
|
RpcQueue.Clear();
|
|
}
|
|
|
|
// Clear networked objects mapping table
|
|
Scripting::ObjectsLookupIdMapping.Set(nullptr);
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectReplicate msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
ScopeLock lock(ObjectsLock);
|
|
if (DespawnedObjects.Contains(msgData.ObjectId))
|
|
return; // Skip replicating not-existing objects
|
|
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.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 (msgData.PartsCount == 1)
|
|
{
|
|
// Replicate
|
|
InvokeObjectReplication(item, msgData.OwnerFrame, event.Message.Buffer + event.Message.Position, msgData.DataSize, senderClientId);
|
|
}
|
|
else
|
|
{
|
|
// Add to replication from multiple parts
|
|
const uint16 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate);
|
|
ReplicateItem* replicateItem = AddObjectReplicateItem(event, msgData, 0, msgMaxData, senderClientId);
|
|
replicateItem->Object = e->Object;
|
|
}
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectReplicatePart msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
ScopeLock lock(ObjectsLock);
|
|
if (DespawnedObjects.Contains(msgData.ObjectId))
|
|
return; // Skip replicating not-existing objects
|
|
|
|
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
|
|
AddObjectReplicateItem(event, msgData, msgData.PartStart, msgData.PartSize, senderClientId);
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectSpawn msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
|
|
if (msgData.ItemsCount == 0)
|
|
return;
|
|
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;
|
|
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 (msgData.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() == msgData.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() == msgData.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(msgData.PrefabId, Prefab::TypeInitializer);
|
|
if (!prefab)
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
|
|
return;
|
|
}
|
|
prefabInstance = PrefabManager::SpawnPrefab(prefab, Transform::Identity, nullptr, nullptr);
|
|
if (!prefabInstance)
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.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(), msgData.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)
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectDespawn msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
ScopeLock lock(ObjectsLock);
|
|
NetworkReplicatedObject* e = ResolveObject(msgData.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 {}", msgData.ObjectId);
|
|
if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
|
|
Hierarchy->RemoveObject(obj);
|
|
DespawnedObjects.Add(msgData.ObjectId);
|
|
if (item.AsNetworkObject)
|
|
item.AsNetworkObject->OnNetworkDespawn();
|
|
Objects.Remove(obj);
|
|
DeleteNetworkObject(obj);
|
|
}
|
|
else
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to despawn object {}", msgData.ObjectId);
|
|
}
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectRole msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
ScopeLock lock(ObjectsLock);
|
|
NetworkReplicatedObject* e = ResolveObject(msgData.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 {}", msgData.ObjectId);
|
|
}
|
|
}
|
|
|
|
void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
|
{
|
|
PROFILE_CPU();
|
|
NetworkMessageObjectRpc msgData;
|
|
event.Message.ReadStructure(msgData);
|
|
ScopeLock lock(ObjectsLock);
|
|
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
|
|
if (e)
|
|
{
|
|
auto& item = *e;
|
|
ScriptingObject* obj = item.Object.Get();
|
|
if (!obj)
|
|
return;
|
|
|
|
// Find RPC info
|
|
NetworkRpcName name;
|
|
name.First = Scripting::FindScriptingType(msgData.RpcTypeName);
|
|
name.Second = msgData.RpcName;
|
|
const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name);
|
|
if (!info)
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(msgData.RpcTypeName), String(msgData.RpcName), msgData.ObjectId);
|
|
return;
|
|
}
|
|
|
|
// Validate RPC
|
|
if (info->Server && NetworkManager::IsClient())
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot invoke server RPC {}::{} on client", String(msgData.RpcTypeName), String(msgData.RpcName));
|
|
return;
|
|
}
|
|
if (info->Client && NetworkManager::IsServer())
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot invoke client RPC {}::{} on server", String(msgData.RpcTypeName), String(msgData.RpcName));
|
|
return;
|
|
}
|
|
|
|
// Setup message reading stream
|
|
if (CachedReadStream == nullptr)
|
|
CachedReadStream = New<NetworkStream>();
|
|
NetworkStream* stream = CachedReadStream;
|
|
stream->SenderId = client ? client->ClientId : NetworkManager::ServerClientId;
|
|
stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.ArgsSize);
|
|
|
|
// Execute RPC
|
|
info->Execute(obj, stream, info->Tag);
|
|
}
|
|
else
|
|
{
|
|
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", msgData.ObjectId, String(msgData.RpcTypeName), String(msgData.RpcName));
|
|
}
|
|
}
|