diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index a1f2a4565..1e4444af0 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -2441,10 +2441,14 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu { if (bucket.LoopsLeft == 0) { - // End playing animation + // End playing animation and reset bucket params value = tryGetValue(node->GetBox(1), Value::Null); bucket.Index = -1; slot.Animation = nullptr; + bucket.TimePosition = 0.0f; + bucket.BlendInPosition = 0.0f; + bucket.BlendOutPosition = 0.0f; + bucket.LoopsDone = 0; return; } diff --git a/Source/Engine/Core/Collections/HashSetBase.h b/Source/Engine/Core/Collections/HashSetBase.h index 36fcf275d..94bbd149d 100644 --- a/Source/Engine/Core/Collections/HashSetBase.h +++ b/Source/Engine/Core/Collections/HashSetBase.h @@ -409,27 +409,36 @@ protected: else { // Rebuild entire table completely + const int32 elementsCount = _elementsCount; + const int32 oldSize = _size; AllocationData oldAllocation; - AllocationUtils::MoveToEmpty(oldAllocation, _allocation, _size, _size); + AllocationUtils::MoveToEmpty(oldAllocation, _allocation, oldSize, oldSize); _allocation.Allocate(_size); BucketType* data = _allocation.Get(); - for (int32 i = 0; i < _size; ++i) + for (int32 i = 0; i < oldSize; ++i) data[i]._state = HashSetBucketState::Empty; BucketType* oldData = oldAllocation.Get(); FindPositionResult pos; - for (int32 i = 0; i < _size; ++i) + for (int32 i = 0; i < oldSize; ++i) { BucketType& oldBucket = oldData[i]; if (oldBucket.IsOccupied()) { FindPosition(oldBucket.GetKey(), pos); - ASSERT(pos.FreeSlotIndex != -1); + if (pos.FreeSlotIndex == -1) + { + // Grow and retry to handle pathological cases (eg. heavy collisions) + EnsureCapacity(_size + 1, true); + FindPosition(oldBucket.GetKey(), pos); + ASSERT(pos.FreeSlotIndex != -1); + } BucketType& bucket = _allocation.Get()[pos.FreeSlotIndex]; bucket = MoveTemp(oldBucket); } } - for (int32 i = 0; i < _size; ++i) + for (int32 i = 0; i < oldSize; ++i) oldData[i].Free(); + _elementsCount = elementsCount; } _deletedCount = 0; } diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index ee66c6c77..343710183 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -554,10 +554,11 @@ void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* ani { for (auto& slot : GraphInstance.Slots) { - if (slot.Animation == anim && slot.Name == slotName) + if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName) { //slot.Animation = nullptr; // TODO: make an immediate version of this method and set the animation to nullptr. - slot.Reset = true; + if (slot.Animation != nullptr) + slot.Reset = true; break; } } @@ -573,7 +574,7 @@ void AnimatedModel::PauseSlotAnimation(const StringView& slotName, Animation* an { for (auto& slot : GraphInstance.Slots) { - if (slot.Animation == anim && slot.Name == slotName) + if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName) { slot.Pause = true; break; @@ -595,7 +596,7 @@ bool AnimatedModel::IsPlayingSlotAnimation(const StringView& slotName, Animation { for (auto& slot : GraphInstance.Slots) { - if (slot.Animation == anim && slot.Name == slotName && !slot.Pause) + if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName && !slot.Pause) return true; } return false; diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index a011be0d0..a520d6723 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -412,8 +412,8 @@ public: /// Stops the animation playback on the slot in Anim Graph. /// /// The name of the slot. - /// The animation to stop. - API_FUNCTION() void StopSlotAnimation(const StringView& slotName, Animation* anim); + /// The animation to check. Null to use slot name only. + API_FUNCTION() void StopSlotAnimation(const StringView& slotName, Animation* anim = nullptr); /// /// Pauses all the animations playback on the all slots in Anim Graph. @@ -424,8 +424,8 @@ public: /// Pauses the animation playback on the slot in Anim Graph. /// /// The name of the slot. - /// The animation to pause. - API_FUNCTION() void PauseSlotAnimation(const StringView& slotName, Animation* anim); + /// The animation to check. Null to use slot name only. + API_FUNCTION() void PauseSlotAnimation(const StringView& slotName, Animation* anim = nullptr); /// /// Checks if any animation playback is active on any slot in Anim Graph (not paused). @@ -436,8 +436,8 @@ public: /// Checks if the animation playback is active on the slot in Anim Graph (not paused). /// /// The name of the slot. - /// The animation to check. - API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim); + /// The animation to check. Null to use slot name only. + API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim = nullptr); private: void ApplyRootMotion(const Transform& rootMotionDelta); diff --git a/Source/Engine/Main/MainUtil.h b/Source/Engine/Main/MainUtil.h index 668c69c3e..9e1a1503e 100644 --- a/Source/Engine/Main/MainUtil.h +++ b/Source/Engine/Main/MainUtil.h @@ -16,7 +16,7 @@ const Char* GetCommandLine(int argc, char* argv[]) const Char* cmdLine; if (length != 0) { - Char* str = (Char*)malloc(length * sizeof(Char)); + Char* str = (Char*)malloc((length + 1) * sizeof(Char)); cmdLine = str; for (int i = 1; i < argc; i++) { diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h index 9db851996..29cf0b30f 100644 --- a/Source/Engine/Networking/NetworkReplicationHierarchy.h +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -100,6 +100,35 @@ API_STRUCT(NoDefault, Namespace="FlaxEngine.Networking") struct FLAXENGINE_API N return Word0 + Word1 != 0; } + NetworkClientsMask operator&(const NetworkClientsMask& other) const + { + return { Word0 & other.Word0, Word1 & other.Word1 }; + } + + NetworkClientsMask operator|(const NetworkClientsMask& other) const + { + return { Word0 | other.Word0, Word1 | other.Word1 }; + } + + NetworkClientsMask operator~() const + { + return { ~Word0, ~Word1 }; + } + + NetworkClientsMask& operator|=(const NetworkClientsMask& other) + { + Word0 |= other.Word0; + Word1 |= other.Word1; + return *this; + } + + NetworkClientsMask& operator&=(const NetworkClientsMask& other) + { + Word0 &= other.Word0; + Word1 &= other.Word1; + return *this; + } + bool operator==(const NetworkClientsMask& other) const { return Word0 == other.Word0 && Word1 == other.Word1; diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index bcca584b8..e5a7d232e 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -152,12 +152,12 @@ struct NetworkReplicatedObject bool operator==(const NetworkReplicatedObject& other) const { - return Object == other.Object; + return ObjectId == other.ObjectId; } bool operator==(const ScriptingObject* other) const { - return Object == other; + return other && ObjectId == other->GetID(); } bool operator==(const Guid& other) const @@ -176,6 +176,11 @@ 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]; @@ -698,14 +703,11 @@ void SendReplication(ScriptingObject* obj, NetworkClientsMask targetClients) return; auto& item = it->Item; const bool isClient = NetworkManager::IsClient(); + const NetworkClientsMask fullTargetClients = targetClients; - // Skip serialization of objects that none will receive - if (!isClient) - { - BuildCachedTargets(item, targetClients); - if (CachedTargets.Count() == 0) - return; - } + // If server has no recipients, skip early. + if (!isClient && !targetClients) + return; if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkSerialize(); @@ -728,18 +730,30 @@ void SendReplication(ScriptingObject* obj, NetworkClientsMask targetClients) #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) + if (item.RepCache.Data.Length() == size && Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0) { - return; + // Check if only newly joined clients are missing this data to avoid resending it to everyone + NetworkClientsMask missingClients = targetClients & ~item.RepCache.Mask; + + // If data is the same and only the client set changed, replicate to missing clients only + if (!missingClients) + return; + targetClients = missingClients; } - item.RepCache.Mask = targetClients; + item.RepCache.Mask = fullTargetClients; 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; + // Skip serialization of objects that none will receive + if (!isClient) + { + BuildCachedTargets(item, targetClients); + if (CachedTargets.Count() == 0) + return; + } + // Send object to clients NetworkMessageObjectReplicate msgData; msgData.OwnerFrame = NetworkManager::Frame; @@ -1530,7 +1544,21 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj) // Register for despawning (batched during update) auto& despawn = DespawnQueue.AddOne(); despawn.Id = obj->GetID(); - despawn.Targets = item.TargetClientIds; + if (item.TargetClientIds.IsValid()) + { + despawn.Targets = item.TargetClientIds; + } + else + { + // Snapshot current recipients to avoid sending despawn to clients that connect later (and never got the spawn) + Array> clientIds; + for (const NetworkClient* client : NetworkManager::Clients) + { + if (client->State == NetworkConnectionState::Connected && client->ClientId != item.OwnerClientId) + clientIds.Add(client->ClientId); + } + despawn.Targets.Copy(clientIds); + } // Prevent spawning for (int32 i = 0; i < SpawnQueue.Count(); i++) @@ -1823,6 +1851,31 @@ void NetworkInternal::NetworkReplicatorClientConnected(NetworkClient* client) { ScopeLock lock(ObjectsLock); NewClients.Add(client); + + // Ensure cached replication acknowledges the new client without resending to others. + // Clear the new client's bit in RepCache and schedule a near-term replication. + const int32 clientIndex = NetworkManager::Clients.Find(client); + if (clientIndex != -1) + { + const uint64 bitMask = 1ull << (uint64)(clientIndex % 64); + const int32 wordIndex = clientIndex / 64; + for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) + { + auto& item = it->Item; + ScriptingObject* obj = item.Object.Get(); + if (!obj || !item.Spawned || item.Role != NetworkObjectRole::OwnedAuthoritative) + continue; + + // Mark this client as missing cached data + uint64* word = wordIndex == 0 ? &item.RepCache.Mask.Word0 : &item.RepCache.Mask.Word1; + *word &= ~bitMask; + + // Force next replication tick for this object so the new client gets data promptly + if (Hierarchy) + Hierarchy->DirtyObject(obj); + } + } + ASSERT(sizeof(NetworkClientsMask) * 8 >= (uint32)NetworkManager::Clients.Count()); // Ensure that clients mask can hold all of clients } @@ -2275,7 +2328,9 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network } else { - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to despawn object {}", objectId); + // If this client never had the object (eg. it was targeted to other clients only), drop the message quietly + DespawnedObjects.Add(objectId); + NETWORK_REPLICATOR_LOG(Warning, "[NetworkReplicator] Failed to despawn object {}", objectId); } }