From 016788c21e5ff5cf6c62e4ddacd849d5f14afd7b Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Fri, 18 Oct 2024 00:00:00 +0000 Subject: [PATCH] com.unity.netcode.gameobjects@2.1.1 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [2.1.1] - 2024-10-18 ### Added - Added ability to edit the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` within the inspector view. (#3097) - Added `IContactEventHandlerWithInfo` that derives from `IContactEventHandler` that can be updated per frame to provide `ContactEventHandlerInfo` information to the `RigidbodyContactEventManager` when processing collisions. (#3094) - `ContactEventHandlerInfo.ProvideNonRigidBodyContactEvents`: When set to true, non-`Rigidbody` collisions with the registered `Rigidbody` will generate contact event notifications. (#3094) - `ContactEventHandlerInfo.HasContactEventPriority`: When set to true, the `Rigidbody` will be prioritized as the instance that generates the event if the `Rigidbody` colliding does not have priority. (#3094) - Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088) - Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088) ### Fixed - Fixed issue where `NetworkPrefabProcessor` would not mark the prefab list as dirty and prevent saving the `DefaultNetworkPrefabs` asset when only imports or only deletes were detected.(#3103) - Fixed an issue where nested `NetworkTransform` components in owner authoritative mode cleared their initial settings on the server, causing improper synchronization. (#3099) - Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096) - Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092) - Fixed an issue where newly synchronizing clients would always receive current `NetworkVariable` values, potentially causing issues with collections if there were pending updates. Now, pending state updates serialize previous values to avoid duplicates on new clients. (#3081) - Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081) - Fixed an issue where transferring ownership of `NetworkVariable` collections didn't update the new owner’s previous value, causing the last added value to be detected as a change during additions or removals. (#3081) - Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081) - Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078) - Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077) - Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075) ### Changed - Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097) - Updated `NetworkVariableDeltaMessage` so the server now forwards delta state updates from clients immediately, instead of waiting until the end of the frame or the next network tick. (#3081) --- CHANGELOG.md | 30 + .../Configuration/NetworkPrefabProcessor.cs | 2 +- Editor/NetworkManagerEditor.cs | 25 +- Runtime/Components/NetworkTransform.cs | 43 +- .../RigidbodyContactEventManager.cs | 168 ++- .../Connection/NetworkConnectionManager.cs | 59 +- Runtime/Core/NetworkBehaviour.cs | 68 +- Runtime/Core/NetworkBehaviourUpdater.cs | 18 +- Runtime/Core/NetworkManager.cs | 26 +- Runtime/Core/NetworkObject.cs | 42 +- Runtime/Core/NetworkObjectRefreshTool.cs | 72 +- .../Messages/ChangeOwnershipMessage.cs | 40 +- .../Messages/ConnectionApprovedMessage.cs | 85 +- .../Messages/ConnectionRequestMessage.cs | 49 +- .../Messages/NetworkVariableDeltaMessage.cs | 308 ++++- .../Collections/NetworkList.cs | 68 +- Runtime/NetworkVariable/NetworkVariable.cs | 95 +- .../NetworkVariable/NetworkVariableBase.cs | 32 + Runtime/SceneManagement/SceneEventData.cs | 8 +- Runtime/Spawning/NetworkSpawnManager.cs | 69 +- TestHelpers/Runtime/NetcodeIntegrationTest.cs | 14 +- Tests/Runtime/ConnectionApproval.cs | 41 +- .../Runtime/ConnectionApprovalTimeoutTests.cs | 2 +- Tests/Runtime/DeferredMessagingTests.cs | 16 +- Tests/Runtime/DisconnectTests.cs | 3 + Tests/Runtime/HiddenVariableTests.cs | 14 +- Tests/Runtime/NetworkManagerEventsTests.cs | 54 + .../NetworkObjectOwnershipTests.cs | 46 +- .../NetworkObjectSpawnManyObjectsTests.cs | 14 +- .../NetworkTransformOwnershipTests.cs | 275 +++++ .../NetworkVariableCollectionsTests.cs | 1024 +++++++++++++++-- Tests/Runtime/Physics/NetworkRigidbodyTest.cs | 383 ++++++ Tests/Runtime/PlayerObjectTests.cs | 134 ++- package.json | 10 +- 34 files changed, 3039 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7213ea9..3423d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [2.1.1] - 2024-10-18 + +### Added + +- Added ability to edit the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` within the inspector view. (#3097) +- Added `IContactEventHandlerWithInfo` that derives from `IContactEventHandler` that can be updated per frame to provide `ContactEventHandlerInfo` information to the `RigidbodyContactEventManager` when processing collisions. (#3094) + - `ContactEventHandlerInfo.ProvideNonRigidBodyContactEvents`: When set to true, non-`Rigidbody` collisions with the registered `Rigidbody` will generate contact event notifications. (#3094) + - `ContactEventHandlerInfo.HasContactEventPriority`: When set to true, the `Rigidbody` will be prioritized as the instance that generates the event if the `Rigidbody` colliding does not have priority. (#3094) +- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088) +- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088) + +### Fixed + +- Fixed issue where `NetworkPrefabProcessor` would not mark the prefab list as dirty and prevent saving the `DefaultNetworkPrefabs` asset when only imports or only deletes were detected.(#3103) +- Fixed an issue where nested `NetworkTransform` components in owner authoritative mode cleared their initial settings on the server, causing improper synchronization. (#3099) +- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096) +- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092) +- Fixed an issue where newly synchronizing clients would always receive current `NetworkVariable` values, potentially causing issues with collections if there were pending updates. Now, pending state updates serialize previous values to avoid duplicates on new clients. (#3081) +- Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081) +- Fixed an issue where transferring ownership of `NetworkVariable` collections didn't update the new owner’s previous value, causing the last added value to be detected as a change during additions or removals. (#3081) +- Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081) +- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078) +- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077) +- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075) + +### Changed + +- Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097) +- Updated `NetworkVariableDeltaMessage` so the server now forwards delta state updates from clients immediately, instead of waiting until the end of the frame or the next network tick. (#3081) + ## [2.0.0] - 2024-09-12 ### Added diff --git a/Editor/Configuration/NetworkPrefabProcessor.cs b/Editor/Configuration/NetworkPrefabProcessor.cs index 879a8c3..55f5fcb 100644 --- a/Editor/Configuration/NetworkPrefabProcessor.cs +++ b/Editor/Configuration/NetworkPrefabProcessor.cs @@ -132,7 +132,7 @@ bool ProcessDeletedAssets(string[] strings) // Process the imported and deleted assets var markDirty = ProcessImportedAssets(importedAssets); - markDirty &= ProcessDeletedAssets(deletedAssets); + markDirty |= ProcessDeletedAssets(deletedAssets); if (markDirty) { diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index 5445b0d..0684d74 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -31,6 +31,7 @@ public class NetworkManagerEditor : NetcodeEditorBase private SerializedProperty m_NetworkTransportProperty; private SerializedProperty m_TickRateProperty; #if MULTIPLAYER_SERVICES_SDK_INSTALLED + private SerializedProperty m_AutoSpawnPlayerPrefabClientSide; private SerializedProperty m_NetworkTopologyProperty; #endif private SerializedProperty m_ClientConnectionBufferTimeoutProperty; @@ -104,6 +105,11 @@ private void Initialize() m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate"); #if MULTIPLAYER_SERVICES_SDK_INSTALLED m_NetworkTopologyProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTopology"); + // Only display the auto spawn property when the distributed authority network topology is selected + if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority) + { + m_AutoSpawnPlayerPrefabClientSide = m_NetworkConfigProperty.FindPropertyRelative("AutoSpawnPlayerPrefabClientSide"); + } #endif m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout"); m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval"); @@ -120,8 +126,6 @@ private void Initialize() #if MULTIPLAYER_TOOLS m_NetworkMessageMetrics = m_NetworkConfigProperty.FindPropertyRelative("NetworkMessageMetrics"); #endif - - m_RpcHashSizeProperty = m_NetworkConfigProperty.FindPropertyRelative("RpcHashSize"); m_PrefabsList = m_NetworkConfigProperty .FindPropertyRelative(nameof(NetworkConfig.Prefabs)) @@ -144,6 +148,11 @@ private void CheckNullProperties() m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate"); #if MULTIPLAYER_SERVICES_SDK_INSTALLED m_NetworkTopologyProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTopology"); + // Only display the auto spawn property when the distributed authority network topology is selected + if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority) + { + m_AutoSpawnPlayerPrefabClientSide = m_NetworkConfigProperty.FindPropertyRelative("AutoSpawnPlayerPrefabClientSide"); + } #endif m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout"); m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval"); @@ -173,10 +182,11 @@ private void DisplayNetworkManagerProperties() if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) { serializedObject.Update(); + EditorGUILayout.PropertyField(m_RunInBackgroundProperty); EditorGUILayout.PropertyField(m_LogLevelProperty); - EditorGUILayout.Space(); + EditorGUILayout.LabelField("Network Settings", EditorStyles.boldLabel); #if MULTIPLAYER_SERVICES_SDK_INSTALLED EditorGUILayout.PropertyField(m_NetworkTopologyProperty); @@ -222,8 +232,17 @@ private void DisplayNetworkManagerProperties() EditorGUILayout.Space(); EditorGUILayout.LabelField("Prefab Settings", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_ForceSamePrefabsProperty); +#if MULTIPLAYER_SERVICES_SDK_INSTALLED + // Only display the auto spawn property when the distributed authority network topology is selected + if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority) + { + EditorGUILayout.PropertyField(m_AutoSpawnPlayerPrefabClientSide, new GUIContent("Auto Spawn Player Prefab")); + } +#endif EditorGUILayout.PropertyField(m_PlayerPrefabProperty, new GUIContent("Default Player Prefab")); + + if (m_NetworkManager.NetworkConfig.HasOldPrefabList()) { EditorGUILayout.HelpBox("Network Prefabs serialized in old format. Migrate to new format to edit the list.", MessageType.Info); diff --git a/Runtime/Components/NetworkTransform.cs b/Runtime/Components/NetworkTransform.cs index b9d8eab..a103b0f 100644 --- a/Runtime/Components/NetworkTransform.cs +++ b/Runtime/Components/NetworkTransform.cs @@ -1463,8 +1463,6 @@ private bool ShouldSynchronizeHalfFloat(ulong targetClientId) // For test logging purposes internal NetworkTransformState SynchronizeState; - // DANGO-TODO: We will want to remove this when we migrate NetworkTransforms to a dedicated internal message - private const ushort k_NetworkTransformStateMagic = 0xf48d; #endregion #region ONSYNCHRONIZE @@ -1489,19 +1487,10 @@ protected override void OnSynchronize(ref BufferSerializer serializer) HalfVectorRotation = new HalfVector4(), HalfVectorScale = new HalfVector3(), NetworkDeltaPosition = new NetworkDeltaPosition(), - }; if (serializer.IsWriter) { - // DANGO-TODO: This magic value is sent to the server in order to identify the network transform. - // The server discards it before forwarding synchronization data to other clients. - if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection) - { - var writer = serializer.GetFastBufferWriter(); - writer.WriteValueSafe(k_NetworkTransformStateMagic); - } - SynchronizeState.IsTeleportingNextFrame = true; var transformToCommit = transform; // If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for @@ -3060,12 +3049,44 @@ protected internal override void InternalOnNetworkSessionSynchronized() base.InternalOnNetworkSessionSynchronized(); } + private void ApplyPlayerTransformState() + { + SynchronizeState.InLocalSpace = InLocalSpace; + SynchronizeState.UseInterpolation = Interpolate; + SynchronizeState.QuaternionSync = UseQuaternionSynchronization; + SynchronizeState.UseHalfFloatPrecision = UseHalfFloatPrecision; + SynchronizeState.QuaternionCompression = UseQuaternionCompression; + SynchronizeState.UsePositionSlerp = SlerpPosition; + } + /// /// For dynamically spawned NetworkObjects, when the non-authority instance's client is already connected and /// the SynchronizeState is still pending synchronization then we want to finalize the synchornization at this time. /// protected internal override void InternalOnNetworkPostSpawn() { + // This is a special case for client-server where a server is spawning an owner authoritative NetworkObject but has yet to serialize anything. + // When the server detects that: + // - We are not in a distributed authority session (DAHost check). + // - This is the first/root NetworkTransform. + // - We are in owner authoritative mode. + // - The NetworkObject is not owned by the server. + // - The SynchronizeState.IsSynchronizing is set to false. + // Then we want to: + // - Force the "IsSynchronizing" flag so the NetworkTransform has its state updated properly and runs through the initialization again. + // - Make sure the SynchronizingState is updated to the instantiated prefab's default flags/settings. + if (NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode && m_IsFirstNetworkTransform && !OnIsServerAuthoritative() && !IsOwner && !SynchronizeState.IsSynchronizing) + { + // Assure the first/root NetworkTransform has the synchronizing flag set so the server runs through the final post initialization steps + SynchronizeState.IsSynchronizing = true; + // Assure the SynchronizeState matches the initial prefab's values for each associated NetworkTransfrom (this includes root + all children) + foreach (var child in NetworkObject.NetworkTransforms) + { + child.ApplyPlayerTransformState(); + } + // Now fall through to the final synchronization portion of the spawning for NetworkTransform + } + if (!CanCommitToTransform && NetworkManager.IsConnectedClient && SynchronizeState.IsSynchronizing) { NonAuthorityFinalizeSynchronization(); diff --git a/Runtime/Components/RigidbodyContactEventManager.cs b/Runtime/Components/RigidbodyContactEventManager.cs index 9471a94..d0808d2 100644 --- a/Runtime/Components/RigidbodyContactEventManager.cs +++ b/Runtime/Components/RigidbodyContactEventManager.cs @@ -6,13 +6,70 @@ namespace Unity.Netcode.Components { + /// + /// Information a returns to via
+ /// if the registers itself with as opposed to . + ///
+ public struct ContactEventHandlerInfo + { + /// + /// When set to true, the will include non-Rigidbody based contact events.
+ /// When the invokes the it will return null in place
+ /// of the collidingBody parameter if the contact event occurred with a collider that is not registered with the . + ///
+ public bool ProvideNonRigidBodyContactEvents; + /// + /// When set to true, the will prioritize invoking

+ /// if it is the 2nd colliding body in the contact pair being processed. With distributed authority, setting this value to true when a is owned by the local client
+ /// will assure is only invoked on the authoritative side. + ///
+ public bool HasContactEventPriority; + } + + /// + /// Default implementation required to register a with a instance. + /// + /// + /// Recommended to implement this method on a component + /// public interface IContactEventHandler { + /// + /// Should return a . + /// Rigidbody GetRigidbody(); + /// + /// Invoked by the instance. + /// + /// A unique contact event identifier. + /// The average normal of the collision between two colliders. + /// If not null, this will be a registered that was part of the collision contact event. + /// The world space location of the contact event. + /// Will be set if this is a collision stay contact event (i.e. it is not the first contact event and continually has contact) + /// The average normal of the collision stay contact over time. void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default); } + /// + /// This is an extended version of and can be used to register a with a instance.
+ /// This provides additional information to the for each set of contact events it is processing. + ///
+ public interface IContactEventHandlerWithInfo : IContactEventHandler + { + /// + /// Invoked by for each set of contact events it is processing (prior to processing). + /// + /// + ContactEventHandlerInfo GetContactEventHandlerInfo(); + } + + /// + /// Add this component to an in-scene placed GameObject to provide faster collision event processing between instances and optionally static colliders. + ///
+ ///
+ ///
+ ///
[AddComponentMenu("Netcode/Rigidbody Contact Event Manager")] public class RigidbodyContactEventManager : MonoBehaviour { @@ -34,6 +91,7 @@ private struct JobResultStruct private readonly Dictionary m_RigidbodyMapping = new Dictionary(); private readonly Dictionary m_HandlerMapping = new Dictionary(); + private readonly Dictionary m_HandlerInfo = new Dictionary(); private void OnEnable() { @@ -49,6 +107,15 @@ private void OnEnable() Instance = this; } + /// + /// Any implementation can register a to be handled by this instance. + /// + /// + /// You should enable for each associated with the being registered.
+ /// You can enable this during run time or within the editor's inspector view. + ///
+ /// or + /// true to register and false to remove from being registered public void RegisterHandler(IContactEventHandler contactEventHandler, bool register = true) { var rigidbody = contactEventHandler.GetRigidbody(); @@ -64,6 +131,22 @@ public void RegisterHandler(IContactEventHandler contactEventHandler, bool regis { m_HandlerMapping.Add(instanceId, contactEventHandler); } + + if (!m_HandlerInfo.ContainsKey(instanceId)) + { + var handlerInfo = new ContactEventHandlerInfo() + { + HasContactEventPriority = true, + ProvideNonRigidBodyContactEvents = false, + }; + var handlerWithInfo = contactEventHandler as IContactEventHandlerWithInfo; + + if (handlerWithInfo != null) + { + handlerInfo = handlerWithInfo.GetContactEventHandlerInfo(); + } + m_HandlerInfo.Add(instanceId, handlerInfo); + } } else { @@ -88,25 +171,98 @@ private void OnDisable() private void ProcessCollisions() { + foreach (var contactEventHandler in m_HandlerMapping) + { + var handlerWithInfo = contactEventHandler.Value as IContactEventHandlerWithInfo; + + if (handlerWithInfo != null) + { + m_HandlerInfo[contactEventHandler.Key] = handlerWithInfo.GetContactEventHandlerInfo(); + } + else + { + var info = m_HandlerInfo[contactEventHandler.Key]; + info.HasContactEventPriority = !m_RigidbodyMapping[contactEventHandler.Key].isKinematic; + m_HandlerInfo[contactEventHandler.Key] = info; + } + } + + ContactEventHandlerInfo contactEventHandlerInfo0; + ContactEventHandlerInfo contactEventHandlerInfo1; + // Process all collisions for (int i = 0; i < m_Count; i++) { var thisInstanceID = m_ResultsArray[i].ThisInstanceID; var otherInstanceID = m_ResultsArray[i].OtherInstanceID; - var rb0Valid = thisInstanceID != 0 && m_RigidbodyMapping.ContainsKey(thisInstanceID); - var rb1Valid = otherInstanceID != 0 && m_RigidbodyMapping.ContainsKey(otherInstanceID); - // Only notify registered rigid bodies. - if (!rb0Valid || !rb1Valid || !m_HandlerMapping.ContainsKey(thisInstanceID)) + var contactHandler0 = (IContactEventHandler)null; + var contactHandler1 = (IContactEventHandler)null; + var preferredContactHandler = (IContactEventHandler)null; + var preferredContactHandlerNonRigidbody = false; + var preferredRigidbody = (Rigidbody)null; + var otherContactHandler = (IContactEventHandler)null; + var otherRigidbody = (Rigidbody)null; + + var otherContactHandlerNonRigidbody = false; + + if (m_RigidbodyMapping.ContainsKey(thisInstanceID)) + { + contactHandler0 = m_HandlerMapping[thisInstanceID]; + contactEventHandlerInfo0 = m_HandlerInfo[thisInstanceID]; + if (contactEventHandlerInfo0.HasContactEventPriority) + { + preferredContactHandler = contactHandler0; + preferredContactHandlerNonRigidbody = contactEventHandlerInfo0.ProvideNonRigidBodyContactEvents; + preferredRigidbody = m_RigidbodyMapping[thisInstanceID]; + } + else + { + otherContactHandler = contactHandler0; + otherContactHandlerNonRigidbody = contactEventHandlerInfo0.ProvideNonRigidBodyContactEvents; + otherRigidbody = m_RigidbodyMapping[thisInstanceID]; + } + } + + if (m_RigidbodyMapping.ContainsKey(otherInstanceID)) + { + contactHandler1 = m_HandlerMapping[otherInstanceID]; + contactEventHandlerInfo1 = m_HandlerInfo[otherInstanceID]; + if (contactEventHandlerInfo1.HasContactEventPriority && preferredContactHandler == null) + { + preferredContactHandler = contactHandler1; + preferredContactHandlerNonRigidbody = contactEventHandlerInfo1.ProvideNonRigidBodyContactEvents; + preferredRigidbody = m_RigidbodyMapping[otherInstanceID]; + } + else + { + otherContactHandler = contactHandler1; + otherContactHandlerNonRigidbody = contactEventHandlerInfo1.ProvideNonRigidBodyContactEvents; + otherRigidbody = m_RigidbodyMapping[otherInstanceID]; + } + } + + if (preferredContactHandler == null && otherContactHandler != null) + { + preferredContactHandler = otherContactHandler; + preferredContactHandlerNonRigidbody = otherContactHandlerNonRigidbody; + preferredRigidbody = otherRigidbody; + otherContactHandler = null; + otherContactHandlerNonRigidbody = false; + otherRigidbody = null; + } + + if (preferredContactHandler == null || (preferredContactHandler != null && otherContactHandler == null && !preferredContactHandlerNonRigidbody)) { continue; } + if (m_ResultsArray[i].HasCollisionStay) { - m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint, m_ResultsArray[i].HasCollisionStay, m_ResultsArray[i].AverageCollisionStayNormal); + preferredContactHandler.ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, otherRigidbody, m_ResultsArray[i].ContactPoint, m_ResultsArray[i].HasCollisionStay, m_ResultsArray[i].AverageCollisionStayNormal); } else { - m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint); + preferredContactHandler.ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, otherRigidbody, m_ResultsArray[i].ContactPoint); } } } diff --git a/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs index b5fff54..5c26df4 100644 --- a/Runtime/Connection/NetworkConnectionManager.cs +++ b/Runtime/Connection/NetworkConnectionManager.cs @@ -105,8 +105,12 @@ internal void InvokeOnClientConnectedCallback(ulong clientId) continue; } - peerClientIds[idx] = peerId; - ++idx; + // This assures if the server has not timed out prior to the client synchronizing that it doesn't exceed the allocated peer count. + if (peerClientIds.Length > idx) + { + peerClientIds[idx] = peerId; + ++idx; + } } try @@ -496,24 +500,32 @@ internal void DisconnectEventHandler(ulong transportClientId) // Process the incoming message queue so that we get everything from the server disconnecting us or, if we are the server, so we got everything from that client. MessageManager.ProcessIncomingMessageQueue(); - InvokeOnClientDisconnectCallback(clientId); - - if (LocalClient.IsHost) - { - InvokeOnPeerDisconnectedCallback(clientId); - } - if (LocalClient.IsServer) { + // We need to process the disconnection before notifying OnClientDisconnectFromServer(clientId); + + // Now notify the client has disconnected + InvokeOnClientDisconnectCallback(clientId); + + if (LocalClient.IsHost) + { + InvokeOnPeerDisconnectedCallback(clientId); + } } - else // As long as we are not in the middle of a shutdown - if (!NetworkManager.ShutdownInProgress) + else { - // We must pass true here and not process any sends messages as we are no longer connected. - // Otherwise, attempting to process messages here can cause an exception within UnityTransport - // as the client ID is no longer valid. - NetworkManager.Shutdown(true); + // Notify local client of disconnection + InvokeOnClientDisconnectCallback(clientId); + + // As long as we are not in the middle of a shutdown + if (!NetworkManager.ShutdownInProgress) + { + // We must pass true here and not process any sends messages as we are no longer connected. + // Otherwise, attempting to process messages here can cause an exception within UnityTransport + // as the client ID is no longer valid. + NetworkManager.Shutdown(true); + } } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.End(); @@ -552,9 +564,6 @@ private void SendConnectionRequest() var message = new ConnectionRequestMessage { CMBServiceConnection = NetworkManager.CMBServiceConnection, - TickRate = NetworkManager.NetworkConfig.TickRate, - EnableSceneManagement = NetworkManager.NetworkConfig.EnableSceneManagement, - // Since only a remote client will send a connection request, we should always force the rebuilding of the NetworkConfig hash value ConfigHash = NetworkManager.NetworkConfig.GetConfig(false), ShouldSendConnectionData = NetworkManager.NetworkConfig.ConnectionApproval, @@ -562,6 +571,12 @@ private void SendConnectionRequest() MessageVersions = new NativeArray(MessageManager.MessageHandlers.Length, Allocator.Temp) }; + if (NetworkManager.CMBServiceConnection) + { + message.ClientConfig.TickRate = NetworkManager.NetworkConfig.TickRate; + message.ClientConfig.EnableSceneManagement = NetworkManager.NetworkConfig.EnableSceneManagement; + } + for (int index = 0; index < MessageManager.MessageHandlers.Length; index++) { if (MessageManager.MessageTypes[index] != null) @@ -739,8 +754,8 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne // Server-side spawning (only if there is a prefab hash or player prefab provided) if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) { - var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault()) - : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault()); + var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position ?? null, response.Rotation ?? null) + : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null); // Spawn the player NetworkObject locally NetworkManager.SpawnManager.SpawnNetworkObjectLocally( @@ -884,7 +899,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne /// /// Client-Side Spawning in distributed authority mode uses this to spawn the player. /// - internal void CreateAndSpawnPlayer(ulong ownerId, Vector3 position = default, Quaternion rotation = default) + internal void CreateAndSpawnPlayer(ulong ownerId) { if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide) { @@ -892,7 +907,7 @@ internal void CreateAndSpawnPlayer(ulong ownerId, Vector3 position = default, Qu if (playerPrefab != null) { var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash; - var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, position, rotation); + var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation); networkObject.IsSceneObject = false; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); } diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index b5d5495..bcdd25f 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -826,6 +826,13 @@ public virtual void OnGainedOwnership() { } internal void InternalOnGainedOwnership() { UpdateNetworkProperties(); + // New owners need to assure any NetworkVariables they have write permissions + // to are updated so the previous and original values are aligned with the + // current value (primarily for collections). + if (OwnerClientId == NetworkManager.LocalClientId) + { + UpdateNetworkVariableOnOwnershipChanged(); + } OnGainedOwnership(); } @@ -1016,9 +1023,14 @@ internal void PreVariableUpdate() internal readonly List NetworkVariableIndexesToReset = new List(); internal readonly HashSet NetworkVariableIndexesToResetSet = new HashSet(); - internal void NetworkVariableUpdate(ulong targetClientId) + /// + /// Determines if a NetworkVariable should have any changes to state sent out + /// + /// target to send the updates to + /// specific to change in ownership + internal void NetworkVariableUpdate(ulong targetClientId, bool forceSend = false) { - if (!CouldHaveDirtyNetworkVariables()) + if (!forceSend && !CouldHaveDirtyNetworkVariables()) { return; } @@ -1069,7 +1081,11 @@ internal void NetworkVariableUpdate(ulong targetClientId) NetworkBehaviourIndex = behaviourIndex, NetworkBehaviour = this, TargetClientId = targetClientId, - DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j] + DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j], + // By sending the network delivery we can forward messages immediately as opposed to processing them + // at the end. While this will send updates to clients that cannot read, the handler will ignore anything + // sent to a client that does not have read permissions. + NetworkDelivery = m_DeliveryTypesForNetworkVariableGroups[j] }; // TODO: Serialization is where the IsDirty flag gets changed. // Messages don't get sent from the server to itself, so if we're host and sending to ourselves, @@ -1114,6 +1130,26 @@ private bool CouldHaveDirtyNetworkVariables() return false; } + /// + /// Invoked on a new client to assure the previous and original values + /// are synchronized with the current known value. + /// + /// + /// Primarily for collections to assure the previous value(s) is/are the + /// same as the current value(s) in order to not re-send already known entries. + /// + internal void UpdateNetworkVariableOnOwnershipChanged() + { + for (int j = 0; j < NetworkVariableFields.Count; j++) + { + // Only invoke OnInitialize on NetworkVariables the owner can write to + if (NetworkVariableFields[j].CanClientWrite(OwnerClientId)) + { + NetworkVariableFields[j].OnInitialize(); + } + } + } + internal void MarkVariablesDirty(bool dirty) { for (int j = 0; j < NetworkVariableFields.Count; j++) @@ -1122,6 +1158,17 @@ internal void MarkVariablesDirty(bool dirty) } } + internal void MarkOwnerReadVariablesDirty() + { + for (int j = 0; j < NetworkVariableFields.Count; j++) + { + if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner) + { + NetworkVariableFields[j].SetDirty(true); + } + } + } + /// /// Synchronizes by setting only the NetworkVariable field values that the client has permission to read. /// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject) @@ -1172,17 +1219,24 @@ internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClie // The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent. writer.WriteValueSafe((ushort)0); var startPos = writer.Position; - NetworkVariableFields[j].WriteField(writer); + // Write the NetworkVariable field value + // WriteFieldSynchronization will write the current value only if there are no pending changes. + // Otherwise, it will write the previous value if there are pending changes since the pending + // changes will be sent shortly after the client's synchronization. + NetworkVariableFields[j].WriteFieldSynchronization(writer); var size = writer.Position - startPos; writer.Seek(writePos); - // Write the NetworkVariable value + // Write the NetworkVariable field value size writer.WriteValueSafe((ushort)size); writer.Seek(startPos + size); } else // Client-Server Only: Should only ever be invoked when using a client-server NetworkTopology { - // Write the NetworkVariable value - NetworkVariableFields[j].WriteField(writer); + // Write the NetworkVariable field value + // WriteFieldSynchronization will write the current value only if there are no pending changes. + // Otherwise, it will write the previous value if there are pending changes since the pending + // changes will be sent shortly after the client's synchronization. + NetworkVariableFields[j].WriteFieldSynchronization(writer); } } else if (ensureLengthSafety) diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index 7bf2030..9062ebf 100644 --- a/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -19,10 +19,15 @@ public class NetworkBehaviourUpdater internal void AddForUpdate(NetworkObject networkObject) { + // Since this is a HashSet, we don't need to worry about duplicate entries m_PendingDirtyNetworkObjects.Add(networkObject); } - internal void NetworkBehaviourUpdate() + /// + /// Sends NetworkVariable deltas + /// + /// internal only, when changing ownership we want to send this before the change in ownership message + internal void NetworkBehaviourUpdate(bool forceSend = false) { #if DEVELOPMENT_BUILD || UNITY_EDITOR m_NetworkBehaviourUpdate.Begin(); @@ -53,7 +58,7 @@ internal void NetworkBehaviourUpdate() // Sync just the variables for just the objects this client sees for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) { - dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId); + dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, forceSend); } } } @@ -72,7 +77,7 @@ internal void NetworkBehaviourUpdate() } for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) { - sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId); + sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, forceSend); } } } @@ -85,19 +90,24 @@ internal void NetworkBehaviourUpdate() var behaviour = dirtyObj.ChildNetworkBehaviours[k]; for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++) { + // Set to true for NetworkVariable to ignore duplication of the + // "internal original value" for collections support. + behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = true; if (behaviour.NetworkVariableFields[i].IsDirty() && !behaviour.NetworkVariableIndexesToResetSet.Contains(i)) { behaviour.NetworkVariableIndexesToResetSet.Add(i); behaviour.NetworkVariableIndexesToReset.Add(i); } + // Reset back to false when done + behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = false; } } } // Now, reset all the no-longer-dirty variables foreach (var dirtyobj in m_DirtyNetworkObjects) { - dirtyobj.PostNetworkVariableWrite(); + dirtyobj.PostNetworkVariableWrite(forceSend); // Once done processing, we set the previous owner id to the current owner id dirtyobj.PreviousOwnerId = dirtyobj.OwnerClientId; } diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 0cdb995..de08ae0 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -5,6 +5,7 @@ using UnityEngine; #if UNITY_EDITOR using UnityEditor; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; #endif using UnityEngine.SceneManagement; using Debug = UnityEngine.Debug; @@ -17,6 +18,17 @@ namespace Unity.Netcode [AddComponentMenu("Netcode/Network Manager", -100)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { + /// + /// Subscribe to this static event to get notifications when a instance has been instantiated. + /// + public static event Action OnInstantiated; + + /// + /// Subscribe to this static event to get notifications when a instance is being destroyed. + /// + public static event Action OnDestroying; + + #if UNITY_EDITOR // Inspector view expand/collapse settings for this derived child class [HideInInspector] @@ -874,6 +886,7 @@ internal T Value internal Override PortOverride; + #if UNITY_EDITOR internal static INetworkManagerHelper NetworkManagerHelper; @@ -900,6 +913,11 @@ protected virtual void OnValidateComponent() } + private PackageInfo GetPackageInfo(string packageName) + { + return AssetDatabase.FindAssets("package").Select(AssetDatabase.GUIDToAssetPath).Where(x => AssetDatabase.LoadAssetAtPath(x) != null).Select(PackageInfo.FindForAssetPath).Where(x => x != null).First(x => x.name == packageName); + } + internal void OnValidate() { if (NetworkConfig == null) @@ -1030,6 +1048,8 @@ private void Awake() #if UNITY_EDITOR EditorApplication.playModeStateChanged += ModeChanged; #endif + // Notify we have instantiated a new instance of NetworkManager. + OnInstantiated?.Invoke(this); } private void OnEnable() @@ -1141,9 +1161,6 @@ internal void Initialize(bool server) UpdateTopology(); - //DANGOEXP TODO: Remove this before finalizing the experimental release - NetworkConfig.AutoSpawnPlayerPrefabClientSide = DistributedAuthorityMode; - // Make sure the ServerShutdownState is reset when initializing if (server) { @@ -1632,6 +1649,9 @@ private void OnDestroy() UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded; + // Notify we are destroying NetworkManager + OnDestroying?.Invoke(this); + if (Singleton == this) { Singleton = null; diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index f89bc1d..3ccd8ea 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -113,11 +113,6 @@ internal void RefreshAllPrefabInstances() } // Handle updating the currently active scene - var networkObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); - foreach (var networkObject in networkObjects) - { - networkObject.OnValidate(); - } NetworkObjectRefreshTool.ProcessActiveScene(); // Refresh all build settings scenes @@ -130,14 +125,14 @@ internal void RefreshAllPrefabInstances() continue; } // Add the scene to be processed - NetworkObjectRefreshTool.ProcessScene(editorScene.path, false); + NetworkObjectRefreshTool.ProcessScene(editorScene.path, true); } // Process all added scenes NetworkObjectRefreshTool.ProcessScenes(); } - private void OnValidate() + internal void OnValidate() { // do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name)) @@ -229,6 +224,7 @@ private void CheckForInScenePlaced() if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) { InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; + EditorUtility.SetDirty(this); } IsSceneObject = true; } @@ -340,7 +336,7 @@ public void DeferDespawn(int tickOffset, bool destroy = true) if (!HasAuthority) { - NetworkLog.LogError($"Only the authoirty can invoke {nameof(DeferDespawn)} and local Client-{NetworkManager.LocalClientId} is not the authority of {name}!"); + NetworkLog.LogError($"Only the authority can invoke {nameof(DeferDespawn)} and local Client-{NetworkManager.LocalClientId} is not the authority of {name}!"); return; } @@ -1613,7 +1609,12 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) { - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); + // If spawning with observers or if not spawning with observers but the observer count is greater than 1 (i.e. owner/authority creating), + // then we want to send a spawn notification. + if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) + { + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); + } } else { @@ -2444,6 +2445,14 @@ internal void MarkVariablesDirty(bool dirty) } } + internal void MarkOwnerReadVariablesDirty() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty(); + } + } + // NGO currently guarantees that the client will receive spawn data for all objects in one network tick. // Children may arrive before their parents; when they do they are stored in OrphanedChildren and then // resolved when their parents arrived. Because we don't send a partial list of spawns (yet), something @@ -2770,11 +2779,11 @@ public void Deserialize(FastBufferReader reader) } } - internal void PostNetworkVariableWrite() + internal void PostNetworkVariableWrite(bool forced = false) { for (int k = 0; k < ChildNetworkBehaviours.Count; k++) { - ChildNetworkBehaviours[k].PostNetworkVariableWrite(); + ChildNetworkBehaviours[k].PostNetworkVariableWrite(forced); } } @@ -3053,10 +3062,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf } } - // Add all known players to the observers list if they don't already exist - foreach (var player in networkManager.SpawnManager.PlayerObjects) + // Only add all other players as observers if we are spawning with observers, + // otherwise user controls via NetworkShow. + if (networkObject.SpawnWithObservers) { - networkObject.Observers.Add(player.OwnerClientId); + // Add all known players to the observers list if they don't already exist + foreach (var player in networkManager.SpawnManager.PlayerObjects) + { + networkObject.Observers.Add(player.OwnerClientId); + } } } } diff --git a/Runtime/Core/NetworkObjectRefreshTool.cs b/Runtime/Core/NetworkObjectRefreshTool.cs index b9c6db0..63d48e9 100644 --- a/Runtime/Core/NetworkObjectRefreshTool.cs +++ b/Runtime/Core/NetworkObjectRefreshTool.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; @@ -21,6 +23,28 @@ internal class NetworkObjectRefreshTool internal static Action AllScenesProcessed; + internal static NetworkObject PrefabNetworkObject; + + internal static void LogInfo(string msg, bool append = false) + { + if (!append) + { + s_Log.AppendLine(msg); + } + else + { + s_Log.Append(msg); + } + } + + internal static void FlushLog() + { + Debug.Log(s_Log.ToString()); + s_Log.Clear(); + } + + private static StringBuilder s_Log = new StringBuilder(); + internal static void ProcessScene(string scenePath, bool processScenes = true) { if (!s_ScenesToUpdate.Contains(scenePath)) @@ -29,7 +53,10 @@ internal static void ProcessScene(string scenePath, bool processScenes = true) { EditorSceneManager.sceneOpened += EditorSceneManager_sceneOpened; EditorSceneManager.sceneSaved += EditorSceneManager_sceneSaved; + s_Log.Clear(); + LogInfo("NetworkObject Refresh Scenes to Process:"); } + LogInfo($"[{scenePath}]", true); s_ScenesToUpdate.Add(scenePath); } s_ProcessScenes = processScenes; @@ -37,6 +64,7 @@ internal static void ProcessScene(string scenePath, bool processScenes = true) internal static void ProcessActiveScene() { + FlushLog(); var activeScene = SceneManager.GetActiveScene(); if (s_ScenesToUpdate.Contains(activeScene.path) && s_ProcessScenes) { @@ -54,10 +82,12 @@ internal static void ProcessScenes() } else { + s_ProcessScenes = false; s_CloseScenes = false; EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved; EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened; AllScenesProcessed?.Invoke(); + FlushLog(); } } @@ -68,9 +98,8 @@ private static void FinishedProcessingScene(Scene scene, bool refreshed = false) // Provide a log of all scenes that were modified to the user if (refreshed) { - Debug.Log($"Refreshed and saved updates to scene: {scene.name}"); + LogInfo($"Refreshed and saved updates to scene: {scene.name}"); } - s_ProcessScenes = false; s_ScenesToUpdate.Remove(scene.path); if (scene != SceneManager.GetActiveScene()) @@ -88,24 +117,41 @@ private static void EditorSceneManager_sceneSaved(Scene scene) private static void SceneOpened(Scene scene) { + LogInfo($"Processing scene {scene.name}:"); if (s_ScenesToUpdate.Contains(scene.path)) { if (s_ProcessScenes) { - if (!EditorSceneManager.MarkSceneDirty(scene)) - { - Debug.Log($"Scene {scene.name} did not get marked as dirty!"); - FinishedProcessingScene(scene); - } - else + var prefabInstances = PrefabUtility.FindAllInstancesOfPrefab(PrefabNetworkObject.gameObject); + + if (prefabInstances.Length > 0) { - EditorSceneManager.SaveScene(scene); + var instancesSceneLoadedSpecific = prefabInstances.Where((c) => c.scene == scene).ToList(); + + if (instancesSceneLoadedSpecific.Count > 0) + { + foreach (var prefabInstance in instancesSceneLoadedSpecific) + { + prefabInstance.GetComponent().OnValidate(); + } + + if (!EditorSceneManager.MarkSceneDirty(scene)) + { + LogInfo($"Scene {scene.name} did not get marked as dirty!"); + FinishedProcessingScene(scene); + } + else + { + LogInfo($"Changes detected and applied!"); + EditorSceneManager.SaveScene(scene); + } + return; + } } } - else - { - FinishedProcessingScene(scene); - } + + LogInfo($"No changes required."); + FinishedProcessingScene(scene); } } diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs index 99010be..d418789 100644 --- a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -9,7 +9,6 @@ internal struct ChangeOwnershipMessage : INetworkMessage, INetworkSerializeByMem public ulong NetworkObjectId; public ulong OwnerClientId; - // DANGOEXP TODO: Remove these notes or change their format // SERVICE NOTES: // When forwarding the message to clients on the CMB Service side, // you can set the ClientIdCount to 0 and skip writing the ClientIds. @@ -258,15 +257,18 @@ public void Handle(ref NetworkContext context) continue; } - // If ownership is changing and this is not an ownership request approval then ignore the OnwerClientId + // If ownership is changing and this is not an ownership request approval then ignore the SenderId + if (OwnershipIsChanging && !RequestApproved && context.SenderId == clientId) + { + continue; + } + // If it is just updating flags then ignore sending to the owner // If it is a request or approving request, then ignore the RequestClientId - if ((OwnershipIsChanging && !RequestApproved && OwnerClientId == clientId) || (OwnershipFlagsUpdate && clientId == OwnerClientId) - || ((RequestOwnership || RequestApproved) && clientId == RequestClientId)) + if ((OwnershipFlagsUpdate && clientId == OwnerClientId) || ((RequestOwnership || RequestApproved) && clientId == RequestClientId)) { continue; } - networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, clientId); } } @@ -327,10 +329,12 @@ private void HandleOwnershipChange(ref NetworkContext context) var networkManager = (NetworkManager)context.SystemOwner; var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; - // DANGO-TODO: This probably shouldn't be allowed to happen. + // Sanity check that we are not sending duplicated change ownership messages if (networkObject.OwnerClientId == OwnerClientId) { - UnityEngine.Debug.LogWarning($"Unnecessary ownership changed message for {NetworkObjectId}"); + UnityEngine.Debug.LogError($"Unnecessary ownership changed message for {NetworkObjectId}."); + // Ignore the message + return; } var originalOwner = networkObject.OwnerClientId; @@ -347,12 +351,6 @@ private void HandleOwnershipChange(ref NetworkContext context) networkObject.InvokeBehaviourOnLostOwnership(); } - // We are new owner or (client-server) or running in distributed authority mode - if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode) - { - networkObject.InvokeBehaviourOnGainedOwnership(); - } - // If in distributed authority mode if (networkManager.DistributedAuthorityMode) { @@ -374,6 +372,22 @@ private void HandleOwnershipChange(ref NetworkContext context) } } + // We are new owner or (client-server) or running in distributed authority mode + if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode) + { + networkObject.InvokeBehaviourOnGainedOwnership(); + } + + + if (originalOwner == networkManager.LocalClientId && !networkManager.DistributedAuthorityMode) + { + // Mark any owner read variables as dirty + networkObject.MarkOwnerReadVariablesDirty(); + // Immediately queue any pending deltas and order the message before the + // change in ownership message. + networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true); + } + // Always invoke ownership change notifications networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId); diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index bb3e446..7b9a87f 100644 --- a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -3,14 +3,39 @@ namespace Unity.Netcode { + internal struct ServiceConfig : INetworkSerializable + { + public uint Version; + public bool IsRestoredSession; + public ulong CurrentSessionOwner; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + if (serializer.IsWriter) + { + BytePacker.WriteValueBitPacked(serializer.GetFastBufferWriter(), Version); + serializer.SerializeValue(ref IsRestoredSession); + BytePacker.WriteValueBitPacked(serializer.GetFastBufferWriter(), CurrentSessionOwner); + } + else + { + ByteUnpacker.ReadValueBitPacked(serializer.GetFastBufferReader(), out Version); + serializer.SerializeValue(ref IsRestoredSession); + ByteUnpacker.ReadValueBitPacked(serializer.GetFastBufferReader(), out CurrentSessionOwner); + } + } + } + internal struct ConnectionApprovedMessage : INetworkMessage { + private const int k_AddCMBServiceConfig = 2; private const int k_VersionAddClientIds = 1; - public int Version => k_VersionAddClientIds; + public int Version => k_AddCMBServiceConfig; public ulong OwnerClientId; public int NetworkTick; // The cloud state service should set this if we are restoring a session + public ServiceConfig ServiceConfig; public bool IsRestoredSession; public ulong CurrentSessionOwner; // Not serialized @@ -25,6 +50,32 @@ internal struct ConnectionApprovedMessage : INetworkMessage public NativeArray ConnectedClientIds; + private int m_ReceiveMessageVersion; + + private ulong GetSessionOwner() + { + if (m_ReceiveMessageVersion >= k_AddCMBServiceConfig) + { + return ServiceConfig.CurrentSessionOwner; + } + else + { + return CurrentSessionOwner; + } + } + + private bool GetIsSessionRestor() + { + if (m_ReceiveMessageVersion >= k_AddCMBServiceConfig) + { + return ServiceConfig.IsRestoredSession; + } + else + { + return IsRestoredSession; + } + } + public void Serialize(FastBufferWriter writer, int targetVersion) { // ============================================================ @@ -45,8 +96,17 @@ public void Serialize(FastBufferWriter writer, int targetVersion) BytePacker.WriteValueBitPacked(writer, NetworkTick); if (IsDistributedAuthority) { - writer.WriteValueSafe(IsRestoredSession); - BytePacker.WriteValueBitPacked(writer, CurrentSessionOwner); + if (targetVersion >= k_AddCMBServiceConfig) + { + ServiceConfig.IsRestoredSession = false; + ServiceConfig.CurrentSessionOwner = CurrentSessionOwner; + writer.WriteNetworkSerializable(ServiceConfig); + } + else + { + writer.WriteValueSafe(IsRestoredSession); + BytePacker.WriteValueBitPacked(writer, CurrentSessionOwner); + } } if (targetVersion >= k_VersionAddClientIds) @@ -122,13 +182,20 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int // ============================================================ // END FORBIDDEN SEGMENT // ============================================================ - + m_ReceiveMessageVersion = receivedMessageVersion; ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId); ByteUnpacker.ReadValueBitPacked(reader, out NetworkTick); if (networkManager.DistributedAuthorityMode) { - reader.ReadValueSafe(out IsRestoredSession); - ByteUnpacker.ReadValueBitPacked(reader, out CurrentSessionOwner); + if (receivedMessageVersion >= k_AddCMBServiceConfig) + { + reader.ReadNetworkSerializable(out ServiceConfig); + } + else + { + reader.ReadValueSafe(out IsRestoredSession); + ByteUnpacker.ReadValueBitPacked(reader, out CurrentSessionOwner); + } } if (receivedMessageVersion >= k_VersionAddClientIds) @@ -157,7 +224,7 @@ public void Handle(ref NetworkContext context) if (networkManager.DistributedAuthorityMode) { - networkManager.SetSessionOwner(CurrentSessionOwner); + networkManager.SetSessionOwner(GetSessionOwner()); if (networkManager.LocalClient.IsSessionOwner && networkManager.NetworkConfig.EnableSceneManagement) { networkManager.SceneManager.InitializeScenesLoaded(); @@ -233,9 +300,9 @@ public void Handle(ref NetworkContext context) // Mark the client being connected networkManager.IsConnectedClient = true; - networkManager.SceneManager.IsRestoringSession = IsRestoredSession; + networkManager.SceneManager.IsRestoringSession = GetIsSessionRestor(); - if (!IsRestoredSession) + if (!networkManager.SceneManager.IsRestoringSession) { // Synchronize the service with the initial session owner's loaded scenes and spawned objects networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId); diff --git a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs index 790791d..d8e60d2 100644 --- a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs +++ b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs @@ -2,15 +2,53 @@ namespace Unity.Netcode { + /// + /// Only used when connecting to the distributed authority service + /// + internal struct ClientConfig : INetworkSerializable + { + /// + /// We start at version 1, where anything less than version 1 on the service side + /// is not bypass feature compatible. + /// + private const int k_BypassFeatureCompatible = 1; + public int Version => k_BypassFeatureCompatible; + public uint TickRate; + public bool EnableSceneManagement; + + // Only gets deserialized but should never be used unless testing + public int RemoteClientVersion; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + if (serializer.IsWriter) + { + var writer = serializer.GetFastBufferWriter(); + BytePacker.WriteValueBitPacked(writer, Version); + BytePacker.WriteValueBitPacked(writer, TickRate); + writer.WriteValueSafe(EnableSceneManagement); + } + else + { + var reader = serializer.GetFastBufferReader(); + ByteUnpacker.ReadValueBitPacked(reader, out RemoteClientVersion); + ByteUnpacker.ReadValueBitPacked(reader, out TickRate); + reader.ReadValueSafe(out EnableSceneManagement); + } + } + } + internal struct ConnectionRequestMessage : INetworkMessage { - public int Version => 0; + // This version update is unidirectional (client to service) and version + // handling occurs on the service side. This serialized data is never sent + // to a host or server. + private const int k_SendClientConfigToService = 1; + public int Version => k_SendClientConfigToService; public ulong ConfigHash; - public bool CMBServiceConnection; - public uint TickRate; - public bool EnableSceneManagement; + public ClientConfig ClientConfig; public byte[] ConnectionData; @@ -36,8 +74,7 @@ public void Serialize(FastBufferWriter writer, int targetVersion) if (CMBServiceConnection) { - writer.WriteValueSafe(TickRate); - writer.WriteValueSafe(EnableSceneManagement); + writer.WriteNetworkSerializable(ClientConfig); } if (ShouldSendConnectionData) diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs index eb6050e..8db084c 100644 --- a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Unity.Collections; namespace Unity.Netcode @@ -10,9 +11,22 @@ namespace Unity.Netcode /// serialization. This is due to the generally amorphous nature of network variable /// deltas, since they're all driven by custom virtual method overloads. /// + /// + /// Version 1: + /// This version -does not- use the "KeepDirty" approach. Instead, the server will forward any state updates + /// to the connected clients that are not the sender or the server itself. Each NetworkVariable state update + /// included, on a per client basis, is first validated that the client can read the NetworkVariable before + /// being added to the m_ForwardUpdates table. + /// Version 0: + /// The original version uses the "KeepDirty" approach in a client-server network topology where the server + /// proxies state updates by "keeping the NetworkVariable(s) dirty" so it will send state updates + /// at the end of the frame (but could delay until the next tick). + /// internal struct NetworkVariableDeltaMessage : INetworkMessage { - public int Version => 0; + private const int k_ServerDeltaForwardingAndNetworkDelivery = 1; + public int Version => k_ServerDeltaForwardingAndNetworkDelivery; + public ulong NetworkObjectId; public ushort NetworkBehaviourIndex; @@ -21,10 +35,62 @@ internal struct NetworkVariableDeltaMessage : INetworkMessage public ulong TargetClientId; public NetworkBehaviour NetworkBehaviour; + public NetworkDelivery NetworkDelivery; + private FastBufferReader m_ReceivedNetworkVariableData; + private bool m_ForwardingMessage; + + private int m_ReceivedMessageVersion; + private const string k_Name = "NetworkVariableDeltaMessage"; + private Dictionary> m_ForwardUpdates; + + private List m_UpdatedNetworkVariables; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteNetworkVariable(ref FastBufferWriter writer, ref NetworkVariableBase networkVariable, bool distributedAuthorityMode, bool ensureNetworkVariableLengthSafety, int nonfragmentedSize, int fragmentedSize) + { + if (ensureNetworkVariableLengthSafety) + { + var tempWriter = new FastBufferWriter(nonfragmentedSize, Allocator.Temp, fragmentedSize); + networkVariable.WriteDelta(tempWriter); + BytePacker.WriteValueBitPacked(writer, tempWriter.Length); + + if (!writer.TryBeginWrite(tempWriter.Length)) + { + throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}"); + } + + tempWriter.CopyTo(writer); + } + else + { + // TODO: Determine if we need to remove this with the 6.1 service updates + if (distributedAuthorityMode) + { + var size_marker = writer.Position; + writer.WriteValueSafe(0); + var start_marker = writer.Position; + networkVariable.WriteDelta(writer); + var end_marker = writer.Position; + writer.Seek(size_marker); + var size = end_marker - start_marker; + if (size == 0) + { + UnityEngine.Debug.LogError($"Invalid write size of zero!"); + } + writer.WriteValueSafe((ushort)size); + writer.Seek(end_marker); + } + else + { + networkVariable.WriteDelta(writer); + } + } + } + public void Serialize(FastBufferWriter writer, int targetVersion) { if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(NetworkObjectId) + FastBufferWriter.GetWriteSize(NetworkBehaviourIndex))) @@ -34,10 +100,67 @@ public void Serialize(FastBufferWriter writer, int targetVersion) var obj = NetworkBehaviour.NetworkObject; var networkManager = obj.NetworkManagerOwner; + var typeName = NetworkBehaviour.__getTypeName(); + var nonFragmentedMessageMaxSize = networkManager.MessageManager.NonFragmentedMessageMaxSize; + var fragmentedMessageMaxSize = networkManager.MessageManager.FragmentedMessageMaxSize; + var ensureNetworkVariableLengthSafety = networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety; + var distributedAuthorityMode = networkManager.DistributedAuthorityMode; BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, NetworkBehaviourIndex); - if (networkManager.DistributedAuthorityMode) + + // If using k_IncludeNetworkDelivery version, then we want to write the network delivery used and if we + // are forwarding state updates then serialize any NetworkVariable states specific to this client. + if (targetVersion >= k_ServerDeltaForwardingAndNetworkDelivery) + { + writer.WriteValueSafe(NetworkDelivery); + // If we are forwarding the message, then proceed to forward state updates specific to the targeted client + if (m_ForwardingMessage) + { + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) + { + writer.WriteValueSafe((ushort)NetworkBehaviour.NetworkVariableFields.Count); + } + + for (int i = 0; i < NetworkBehaviour.NetworkVariableFields.Count; i++) + { + var startingSize = writer.Length; + var networkVariable = NetworkBehaviour.NetworkVariableFields[i]; + var shouldWrite = m_ForwardUpdates[TargetClientId].Contains(i); + + // This var does not belong to the currently iterating delivery group. + if (distributedAuthorityMode) + { + if (!shouldWrite) + { + writer.WriteValueSafe(0); + } + } + else if (ensureNetworkVariableLengthSafety) + { + if (!shouldWrite) + { + BytePacker.WriteValueBitPacked(writer, (ushort)0); + } + } + else + { + writer.WriteValueSafe(shouldWrite); + } + + if (shouldWrite) + { + WriteNetworkVariable(ref writer, ref networkVariable, distributedAuthorityMode, ensureNetworkVariableLengthSafety, nonFragmentedMessageMaxSize, fragmentedMessageMaxSize); + networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(TargetClientId, obj, networkVariable.Name, typeName, writer.Length - startingSize); + } + } + return; + } + } + + // DANGO TODO: Remove this when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) { writer.WriteValueSafe((ushort)NetworkBehaviour.NetworkVariableFields.Count); } @@ -46,12 +169,12 @@ public void Serialize(FastBufferWriter writer, int targetVersion) { if (!DeliveryMappedNetworkVariableIndex.Contains(i)) { - // This var does not belong to the currently iterating delivery group. - if (networkManager.DistributedAuthorityMode) + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) { writer.WriteValueSafe(0); } - else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + else if (ensureNetworkVariableLengthSafety) { BytePacker.WriteValueBitPacked(writer, (ushort)0); } @@ -88,14 +211,15 @@ public void Serialize(FastBufferWriter writer, int targetVersion) shouldWrite = false; } - if (networkManager.DistributedAuthorityMode) + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) { if (!shouldWrite) { writer.WriteValueSafe(0); } } - else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + else if (ensureNetworkVariableLengthSafety) { if (!shouldWrite) { @@ -109,53 +233,22 @@ public void Serialize(FastBufferWriter writer, int targetVersion) if (shouldWrite) { - if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) - { - var tempWriter = new FastBufferWriter(networkManager.MessageManager.NonFragmentedMessageMaxSize, Allocator.Temp, networkManager.MessageManager.FragmentedMessageMaxSize); - NetworkBehaviour.NetworkVariableFields[i].WriteDelta(tempWriter); - BytePacker.WriteValueBitPacked(writer, tempWriter.Length); - - if (!writer.TryBeginWrite(tempWriter.Length)) - { - throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}"); - } - - tempWriter.CopyTo(writer); - } - else - { - if (networkManager.DistributedAuthorityMode) - { - var size_marker = writer.Position; - writer.WriteValueSafe(0); - var start_marker = writer.Position; - networkVariable.WriteDelta(writer); - var end_marker = writer.Position; - writer.Seek(size_marker); - var size = end_marker - start_marker; - writer.WriteValueSafe((ushort)size); - writer.Seek(end_marker); - } - else - { - networkVariable.WriteDelta(writer); - } - } - networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( - TargetClientId, - obj, - networkVariable.Name, - NetworkBehaviour.__getTypeName(), - writer.Length - startingSize); + WriteNetworkVariable(ref writer, ref networkVariable, distributedAuthorityMode, ensureNetworkVariableLengthSafety, nonFragmentedMessageMaxSize, fragmentedMessageMaxSize); + networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(TargetClientId, obj, networkVariable.Name, typeName, writer.Length - startingSize); } } } public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) { + m_ReceivedMessageVersion = receivedMessageVersion; ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId); ByteUnpacker.ReadValueBitPacked(reader, out NetworkBehaviourIndex); - + // If we are using the k_IncludeNetworkDelivery message version, then read the NetworkDelivery used + if (receivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery) + { + reader.ReadValueSafe(out NetworkDelivery); + } m_ReceivedNetworkVariableData = reader; return true; @@ -167,7 +260,12 @@ public void Handle(ref NetworkContext context) if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject)) { + var distributedAuthorityMode = networkManager.DistributedAuthorityMode; + var ensureNetworkVariableLengthSafety = networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety; var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex); + var isServerAndDeltaForwarding = m_ReceivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery && networkManager.IsServer; + var markNetworkVariableDirty = m_ReceivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery ? false : networkManager.IsServer; + m_UpdatedNetworkVariables = new List(); if (networkBehaviour == null) { @@ -178,7 +276,8 @@ public void Handle(ref NetworkContext context) } else { - if (networkManager.DistributedAuthorityMode) + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) { m_ReceivedNetworkVariableData.ReadValueSafe(out ushort variableCount); if (variableCount != networkBehaviour.NetworkVariableFields.Count) @@ -187,10 +286,30 @@ public void Handle(ref NetworkContext context) } } + // (For client-server) As opposed to worrying about adding additional processing on the server to send NetworkVariable + // updates at the end of the frame, we now track all NetworkVariable state updates, per client, that need to be forwarded + // to the client. This creates a list of all remaining connected clients that could have updates applied. + if (isServerAndDeltaForwarding) + { + m_ForwardUpdates = new Dictionary>(); + foreach (var clientId in networkManager.ConnectedClientsIds) + { + if (clientId == context.SenderId || clientId == networkManager.LocalClientId || !networkObject.Observers.Contains(clientId)) + { + continue; + } + m_ForwardUpdates.Add(clientId, new List()); + } + } + + // Update NetworkVariable Fields for (int i = 0; i < networkBehaviour.NetworkVariableFields.Count; i++) { int varSize = 0; - if (networkManager.DistributedAuthorityMode) + var networkVariable = networkBehaviour.NetworkVariableFields[i]; + + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode) { m_ReceivedNetworkVariableData.ReadValueSafe(out ushort variableSize); varSize = variableSize; @@ -200,10 +319,9 @@ public void Handle(ref NetworkContext context) continue; } } - else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + else if (ensureNetworkVariableLengthSafety) { ByteUnpacker.ReadValueBitPacked(m_ReceivedNetworkVariableData, out varSize); - if (varSize == 0) { continue; @@ -218,8 +336,6 @@ public void Handle(ref NetworkContext context) } } - var networkVariable = networkBehaviour.NetworkVariableFields[i]; - if (networkManager.IsServer && !networkVariable.CanClientWrite(context.SenderId)) { // we are choosing not to fire an exception here, because otherwise a malicious client could use this to crash the server @@ -247,13 +363,58 @@ public void Handle(ref NetworkContext context) NetworkLog.LogError($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. No more variables can be read. This is critical. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}"); NetworkLog.LogError($"[{networkVariable.GetType().Name}]"); } - return; } int readStartPos = m_ReceivedNetworkVariableData.Position; - // Read Delta so we also notify any subscribers to a change in the NetworkVariable - networkVariable.ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer); + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode || ensureNetworkVariableLengthSafety) + { + var remainingBufferSize = m_ReceivedNetworkVariableData.Length - m_ReceivedNetworkVariableData.Position; + if (varSize > (remainingBufferSize)) + { + UnityEngine.Debug.LogError($"[{networkBehaviour.name}][Delta State Read Error] Expecting to read {varSize} but only {remainingBufferSize} remains!"); + return; + } + } + + // Added a try catch here to assure any failure will only fail on this one message and not disrupt the stack + try + { + // Read the delta + networkVariable.ReadDelta(m_ReceivedNetworkVariableData, markNetworkVariableDirty); + + // Add the NetworkVariable field index so we can invoke the PostDeltaRead + m_UpdatedNetworkVariables.Add(i); + } + catch (Exception ex) + { + UnityEngine.Debug.LogException(ex); + return; + } + + // (For client-server) As opposed to worrying about adding additional processing on the server to send NetworkVariable + // updates at the end of the frame, we now track all NetworkVariable state updates, per client, that need to be forwarded + // to the client. This happens once the server is finished processing all state updates for this message. + if (isServerAndDeltaForwarding) + { + foreach (var forwardEntry in m_ForwardUpdates) + { + // Only track things that the client can read + if (networkVariable.CanClientRead(forwardEntry.Key)) + { + // If the object is about to be shown to the client then don't send an update as it will + // send a full update when shown. + if (networkManager.SpawnManager.ObjectsToShowToClient.ContainsKey(forwardEntry.Key) && + networkManager.SpawnManager.ObjectsToShowToClient[forwardEntry.Key] + .Contains(networkObject)) + { + continue; + } + forwardEntry.Value.Add(i); + } + } + } networkManager.NetworkMetrics.TrackNetworkVariableDeltaReceived( context.SenderId, @@ -262,7 +423,8 @@ public void Handle(ref NetworkContext context) networkBehaviour.__getTypeName(), context.MessageSize); - if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety || networkManager.DistributedAuthorityMode) + // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff + if (distributedAuthorityMode || ensureNetworkVariableLengthSafety) { if (m_ReceivedNetworkVariableData.Position > (readStartPos + varSize)) { @@ -284,6 +446,40 @@ public void Handle(ref NetworkContext context) } } } + + // If we are using the version of this message that includes network delivery, then + // forward this update to all connected clients (other than the sender and the server). + if (isServerAndDeltaForwarding) + { + var message = new NetworkVariableDeltaMessage() + { + NetworkBehaviour = networkBehaviour, + NetworkBehaviourIndex = NetworkBehaviourIndex, + NetworkObjectId = NetworkObjectId, + m_ForwardingMessage = true, + m_ForwardUpdates = m_ForwardUpdates, + }; + + foreach (var forwardEntry in m_ForwardUpdates) + { + // Only forward updates to any client that has visibility to the state updates included in this message + if (forwardEntry.Value.Count > 0) + { + message.TargetClientId = forwardEntry.Key; + networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery, forwardEntry.Key); + } + } + } + + // This should be always invoked (client & server) to assure the previous values are set + // !! IMPORTANT ORDER OF OPERATIONS !! (Has to happen after forwarding deltas) + // When a server forwards delta updates to connected clients, it needs to preserve the previous value + // until it is done serializing all valid NetworkVariable field deltas (relative to each client). This + // is invoked after it is done forwarding the deltas. + foreach (var fieldIndex in m_UpdatedNetworkVariables) + { + networkBehaviour.NetworkVariableFields[fieldIndex].PostDeltaRead(); + } } } else diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index 130cf4e..203632c 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -177,6 +177,13 @@ public override void ReadField(FastBufferReader reader) /// public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { + /// This is only invoked by and the only time + /// keepDirtyDelta is set is when it is the server processing. To be able to handle previous + /// versions, we use IsServer to keep the dirty states received and the keepDirtyDelta to + /// actually mark this as dirty and add it to the list of s to + /// be updated. With the forwarding of deltas being handled by , + /// once all clients have been forwarded the dirty events, we clear them by invoking . + var isServer = m_NetworkManager.IsServer; reader.ReadValueSafe(out ushort deltaCount); for (int i = 0; i < deltaCount; i++) { @@ -199,7 +206,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -207,7 +214,11 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) Index = m_List.Length - 1, Value = m_List[m_List.Length - 1] }); - MarkNetworkObjectDirty(); + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -237,7 +248,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -245,7 +256,11 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) Index = index, Value = m_List[index] }); - MarkNetworkObjectDirty(); + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -271,7 +286,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -279,7 +294,11 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) Index = index, Value = value }); - MarkNetworkObjectDirty(); + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -299,7 +318,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -307,7 +326,11 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) Index = index, Value = value }); - MarkNetworkObjectDirty(); + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -335,7 +358,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -344,7 +367,11 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) Value = value, PreviousValue = previousValue }); - MarkNetworkObjectDirty(); + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -361,13 +388,18 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType }); - MarkNetworkObjectDirty(); + + // Preserve the legacy way of handling this + if (keepDirtyDelta) + { + MarkNetworkObjectDirty(); + } } } break; @@ -381,6 +413,18 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) } } + /// + /// + /// For NetworkList, we just need to reset dirty if a server has read deltas + /// + internal override void PostDeltaRead() + { + if (m_NetworkManager.IsServer) + { + ResetDirty(); + } + } + /// public IEnumerator GetEnumerator() { diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index a98197d..16ca42a 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -41,6 +41,7 @@ public override void OnInitialize() base.OnInitialize(); m_HasPreviousValue = true; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); } @@ -58,6 +59,7 @@ public NetworkVariable(T value = default, : base(readPerm, writePerm) { m_InternalValue = value; + m_InternalOriginalValue = default; // Since we start with IsDirty = true, this doesn't need to be duplicated // right away. It won't get read until after ResetDirty() is called, and // the duplicate will be made there. Avoiding calling @@ -76,6 +78,7 @@ public void Reset(T value = default) if (m_NetworkBehaviour == null || m_NetworkBehaviour != null && !m_NetworkBehaviour.NetworkObject.IsSpawned) { m_InternalValue = value; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); m_PreviousValue = default; } } @@ -86,6 +89,12 @@ public void Reset(T value = default) [SerializeField] private protected T m_InternalValue; + // The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the + // collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the + // lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally + // which can cause a myriad of issues. + private protected T m_InternalOriginalValue; + private protected T m_PreviousValue; private bool m_HasPreviousValue; @@ -116,6 +125,7 @@ public virtual T Value { T previousValue = m_InternalValue; m_InternalValue = value; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); SetDirty(true); m_IsDisposed = false; OnValueChanged?.Invoke(previousValue, m_InternalValue); @@ -136,6 +146,17 @@ public bool CheckDirtyState(bool forceCheck = false) { var isDirty = base.IsDirty(); + // A client without permissions invoking this method should only check to assure the current value is equal to the last known current value + if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId)) + { + // If modifications are detected, then revert back to the last known current value + if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue)) + { + NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); + } + return false; + } + // Compare the previous with the current if not dirty or forcing a check. if ((!isDirty || forceCheck) && !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue)) { @@ -166,6 +187,7 @@ public override void Dispose() } m_InternalValue = default; + m_InternalOriginalValue = default; if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable) { m_HasPreviousValue = false; @@ -188,6 +210,13 @@ public override void Dispose() /// Whether or not the container is dirty public override bool IsDirty() { + // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert + // to the original collection value prior to applying updates (primarily for collections). + if (!NetworkUpdaterCheck && m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue)) + { + NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); + return true; + } // For most cases we can use the dirty flag. // This doesn't work for cases where we're wrapping more complex types // like INetworkSerializable, NativeList, NativeArray, etc. @@ -199,11 +228,11 @@ public override bool IsDirty() return true; } + var dirty = !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue); // Cache the dirty value so we don't perform this again if we already know we're dirty // Unfortunately we can't cache the NOT dirty state, because that might change // in between to checks... but the DIRTY state won't change until ResetDirty() // is called. - var dirty = !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue); SetDirty(dirty); return dirty; } @@ -221,6 +250,8 @@ public override void ResetDirty() { m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + // Once updated, assure the original current value is updated for future comparison purposes + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); } base.ResetDirty(); } @@ -241,16 +272,20 @@ public override void WriteDelta(FastBufferWriter writer) /// Whether or not the container should keep the dirty delta, or mark the delta as consumed public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { - // In order to get managed collections to properly have a previous and current value, we have to - // duplicate the collection at this point before making any modifications to the current. - m_HasPreviousValue = true; - NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert + // to the original collection value prior to applying updates (primarily for collections). + if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue)) + { + NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); + } + NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue); - // todo: // keepDirtyDelta marks a variable received as dirty and causes the server to send the value to clients // In a prefect world, whether a variable was A) modified locally or B) received and needs retransmit // would be stored in different fields + // LEGACY NOTE: This is only to handle NetworkVariableDeltaMessage Version 0 connections. The updated + // NetworkVariableDeltaMessage no longer uses this approach. if (keepDirtyDelta) { SetDirty(true); @@ -259,10 +294,43 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); } + /// + /// This should be always invoked (client & server) to assure the previous values are set + /// !! IMPORTANT !! + /// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s) + /// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked + /// after it is done forwarding the deltas at the end of the method. + /// + internal override void PostDeltaRead() + { + // In order to get managed collections to properly have a previous and current value, we have to + // duplicate the collection at this point before making any modifications to the current. + m_HasPreviousValue = true; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + // Once updated, assure the original current value is updated for future comparison purposes + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); + } + /// public override void ReadField(FastBufferReader reader) { + // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert + // to the original collection value prior to applying updates (primarily for collections). + if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue)) + { + NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); + } + NetworkVariableSerialization.Read(reader, ref m_InternalValue); + // In order to get managed collections to properly have a previous and current value, we have to + // duplicate the collection at this point before making any modifications to the current. + // We duplicate the final value after the read (for ReadField ONLY) so the previous value is at par + // with the current value (since this is only invoked when initially synchronizing). + m_HasPreviousValue = true; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + + // Once updated, assure the original current value is updated for future comparison purposes + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); } /// @@ -270,5 +338,20 @@ public override void WriteField(FastBufferWriter writer) { NetworkVariableSerialization.Write(writer, ref m_InternalValue); } + + internal override void WriteFieldSynchronization(FastBufferWriter writer) + { + // If we have a pending update, then synchronize the client with the previously known + // value since the updated version will be sent on the next tick or next time it is + // set to be updated + if (base.IsDirty() && m_HasPreviousValue) + { + NetworkVariableSerialization.Write(writer, ref m_PreviousValue); + } + else + { + base.WriteFieldSynchronization(writer); + } + } } } diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index 75ce48a..8802397 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -251,6 +251,12 @@ public virtual void ResetDirty() m_IsDirty = false; } + /// + /// Only used during the NetworkBehaviourUpdater pass and only used for NetworkVariable. + /// This is to bypass duplication of the "original internal value" for collections. + /// + internal bool NetworkUpdaterCheck; + /// /// Gets Whether or not the container is dirty /// @@ -341,6 +347,32 @@ internal ulong OwnerClientId() /// Whether or not the delta should be kept as dirty or consumed public abstract void ReadDelta(FastBufferReader reader, bool keepDirtyDelta); + /// + /// This should be always invoked (client & server) to assure the previous values are set + /// !! IMPORTANT !! + /// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s) + /// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked + /// after it is done forwarding the deltas at the end of the method. + /// + internal virtual void PostDeltaRead() + { + } + + /// + /// There are scenarios, specifically with collections, where a client could be synchronizing and + /// some NetworkVariables have pending updates. To avoid duplicating entries, this is invoked only + /// when sending the full synchronization information. + /// + /// + /// Derrived classes should send the previous value for synchronization so when the updated value + /// is sent (after synchronizing the client) it will apply the updates. + /// + /// + internal virtual void WriteFieldSynchronization(FastBufferWriter writer) + { + WriteField(writer); + } + /// /// Virtual implementation /// diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs index 18ac6a7..613c216 100644 --- a/Runtime/SceneManagement/SceneEventData.cs +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -320,9 +320,11 @@ private void SortParentedNetworkObjects() internal void AddSpawnedNetworkObjects() { m_NetworkObjectsSync.Clear(); + // If distributed authority mode and sending to the service, then ignore observers + var distributedAuthoritySendingToService = m_NetworkManager.DistributedAuthorityMode && TargetClientId == NetworkManager.ServerClientId; foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList) { - if (sobj.Observers.Contains(TargetClientId)) + if (sobj.Observers.Contains(TargetClientId) || distributedAuthoritySendingToService) { m_NetworkObjectsSync.Add(sobj); } @@ -666,12 +668,14 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) // Write our count place holder (must not be packed!) writer.WriteValueSafe((ushort)0); var distributedAuthority = m_NetworkManager.DistributedAuthorityMode; + // If distributed authority mode and sending to the service, then ignore observers + var distributedAuthoritySendingToService = distributedAuthority && TargetClientId == NetworkManager.ServerClientId; foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects) { foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) { - if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId)) + if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId) || distributedAuthoritySendingToService) { // Serialize the NetworkObject var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId, distributedAuthority); diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index d3b2f9a..c17e056 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -72,11 +72,22 @@ private void AddPlayerObject(NetworkObject playerObject) return; } } + foreach (var player in m_PlayerObjects) { - player.Observers.Add(playerObject.OwnerClientId); - playerObject.Observers.Add(player.OwnerClientId); + // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer. + if (player.SpawnWithObservers) + { + player.Observers.Add(playerObject.OwnerClientId); + } + + // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object. + if (playerObject.SpawnWithObservers) + { + playerObject.Observers.Add(player.OwnerClientId); + } } + m_PlayerObjects.Add(playerObject); if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) { @@ -423,8 +434,31 @@ internal void RemoveOwnership(NetworkObject networkObject) ChangeOwnership(networkObject, NetworkManager.ServerClientId, true); } + private Dictionary m_LastChangeInOwnership = new Dictionary(); + private const int k_MaximumTickOwnershipChangeMultiplier = 6; + internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false) { + // For client-server: + // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes, + // then notify the user they could potentially lose state updates if developer logging is enabled. + if (!NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup) + { + var hasNetworkVariables = false; + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + hasNetworkVariables = networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0; + if (hasNetworkVariables) + { + break; + } + } + if (hasNetworkVariables && NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss."); + } + } + if (NetworkManager.DistributedAuthorityMode) { // If are not authorized and this is not an approved ownership change, then check to see if we can change ownership @@ -497,15 +531,21 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool // Always notify locally on the server when ownership is lost networkObject.InvokeBehaviourOnLostOwnership(); - networkObject.MarkVariablesDirty(true); - NetworkManager.BehaviourUpdater.AddForUpdate(networkObject); - // Authority adds entries for all client ownership UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); // Always notify locally on the server when a new owner is assigned networkObject.InvokeBehaviourOnGainedOwnership(); + if (networkObject.PreviousOwnerId == NetworkManager.LocalClientId) + { + // Mark any owner read variables as dirty + networkObject.MarkOwnerReadVariablesDirty(); + // Immediately queue any pending deltas and order the message before the + // change in ownership message. + NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true); + } + var size = 0; if (NetworkManager.DistributedAuthorityMode) { @@ -569,6 +609,17 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership /// change can be sent from NetworkBehaviours that override the networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); + + // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. + if (!NetworkManager.DistributedAuthorityMode) + { + if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId)) + { + m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f); + } + var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; + m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier); + } } internal bool HasPrefab(NetworkObject.SceneObject sceneObject) @@ -695,14 +746,14 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. /// - internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3 position = default, Quaternion rotation = default, bool isScenePlaced = false) + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) { NetworkObject networkObject = null; // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position, rotation); + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default); networkObject.NetworkManagerOwner = NetworkManager; } else @@ -752,8 +803,10 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow } else { - // Create prefab instance + // Create prefab instance while applying any pre-assigned position and rotation values networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); + networkObject.transform.position = position ?? networkObject.transform.position; + networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; networkObject.NetworkManagerOwner = NetworkManager; networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash; } diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 70ed784..a84a7cc 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -24,10 +24,11 @@ public abstract class NetcodeIntegrationTest /// Used to determine if a NetcodeIntegrationTest is currently running to /// determine how clients will load scenes /// + protected const float k_DefaultTimeoutPeriod = 8.0f; + protected const float k_TickFrequency = 1.0f / k_DefaultTickRate; internal static bool IsRunning { get; private set; } - - protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f); - protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); + protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(k_DefaultTimeoutPeriod); + protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(k_TickFrequency); public NetcodeLogAssert NetcodeLogAssert; public enum SceneManagementState @@ -544,9 +545,14 @@ protected IEnumerator CreateAndStartNewClient() private bool AllPlayerObjectClonesSpawned(NetworkManager joinedClient) { m_InternalErrorLog.Clear(); + // If we are not checking for spawned players then exit early with a success + if (!ShouldCheckForSpawnedPlayers()) + { + return true; + } + // Continue to populate the PlayerObjects list until all player object (local and clone) are found ClientNetworkManagerPostStart(joinedClient); - var playerObjectRelative = m_ServerNetworkManager.SpawnManager.PlayerObjects.Where((c) => c.OwnerClientId == joinedClient.LocalClientId).FirstOrDefault(); if (playerObjectRelative == null) { diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs index 6fd311f..efa19cf 100644 --- a/Tests/Runtime/ConnectionApproval.cs +++ b/Tests/Runtime/ConnectionApproval.cs @@ -4,6 +4,7 @@ using System.Text; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests @@ -12,9 +13,10 @@ namespace Unity.Netcode.RuntimeTests [TestFixture(PlayerCreation.PrefabHash)] [TestFixture(PlayerCreation.NoPlayer)] [TestFixture(PlayerCreation.FailValidation)] - internal class ConnectionApprovalTests : NetcodeIntegrationTest + internal class ConnectionApprovalTests : IntegrationTestWithApproximation { private const string k_InvalidToken = "Invalid validation token!"; + public enum PlayerCreation { Prefab, @@ -24,6 +26,8 @@ public enum PlayerCreation } private PlayerCreation m_PlayerCreation; private bool m_ClientDisconnectReasonValidated; + private Vector3 m_ExpectedPosition; + private Quaternion m_ExpectedRotation; private Dictionary m_Validated = new Dictionary(); @@ -43,6 +47,12 @@ protected override bool ShouldCheckForSpawnedPlayers() protected override void OnServerAndClientsCreated() { + if (m_PlayerCreation == PlayerCreation.Prefab || m_PlayerCreation == PlayerCreation.PrefabHash) + { + m_ExpectedPosition = GetRandomVector3(-10.0f, 10.0f); + m_ExpectedRotation = Quaternion.Euler(GetRandomVector3(-359.98f, 359.98f)); + } + m_ClientDisconnectReasonValidated = false; m_BypassConnectionTimeout = m_PlayerCreation == PlayerCreation.FailValidation; m_Validated.Clear(); @@ -104,11 +114,36 @@ private bool ClientAndHostValidated() return true; } + private bool ValidatePlayersPositionRotation() + { + foreach (var playerEntries in m_PlayerNetworkObjects) + { + foreach (var player in playerEntries.Value) + { + if (!Approximately(player.Value.transform.position, m_ExpectedPosition)) + { + return false; + } + if (!Approximately(player.Value.transform.rotation, m_ExpectedRotation)) + { + return false; + } + } + } + return true; + } + [UnityTest] public IEnumerator ConnectionApproval() { yield return WaitForConditionOrTimeOut(ClientAndHostValidated); AssertOnTimeout("Timed out waiting for all clients to be approved!"); + + if (m_PlayerCreation == PlayerCreation.Prefab || m_PlayerCreation == PlayerCreation.PrefabHash) + { + yield return WaitForConditionOrTimeOut(ValidatePlayersPositionRotation); + AssertOnTimeout("Not all player prefabs spawned in the correct position and/or rotation!"); + } } private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) @@ -127,8 +162,8 @@ private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.Conn } response.CreatePlayerObject = ShouldCheckForSpawnedPlayers(); - response.Position = null; - response.Rotation = null; + response.Position = m_ExpectedPosition; + response.Rotation = m_ExpectedRotation; response.PlayerPrefabHash = m_PlayerCreation == PlayerCreation.PrefabHash ? m_PlayerPrefab.GetComponent().GlobalObjectIdHash : null; } diff --git a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs index d324bad..db1210f 100644 --- a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs +++ b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs @@ -82,7 +82,7 @@ protected override IEnumerator OnStartedServerAndClients() public IEnumerator ValidateApprovalTimeout() { // Just delay for a second - yield return new WaitForSeconds(1); + yield return new WaitForSeconds(k_TestTimeoutPeriod * 0.25f); // Verify we haven't received the time out message yet NetcodeLogAssert.LogWasNotReceived(LogType.Log, m_ExpectedLogMessage); diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs index cda0dd6..bc97fd7 100644 --- a/Tests/Runtime/DeferredMessagingTests.cs +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -670,7 +670,7 @@ public void WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnS serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - WaitForAllClientsToReceive(); + WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -678,9 +678,9 @@ public void WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnS Assert.IsTrue(manager.DeferMessageCalled); Assert.IsFalse(manager.ProcessTriggersCalled); - Assert.AreEqual(4, manager.DeferredMessageCountTotal()); - Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn)); - Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(3, manager.DeferredMessageCountTotal()); + Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab)); AddPrefabsToClient(client); } @@ -812,7 +812,7 @@ public void WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingTh serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - WaitForAllClientsToReceive(); + WaitForAllClientsToReceive(); // Validate messages are deferred and pending foreach (var client in m_ClientNetworkManagers) @@ -821,10 +821,10 @@ public void WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingTh Assert.IsTrue(manager.DeferMessageCalled); Assert.IsFalse(manager.ProcessTriggersCalled); - Assert.AreEqual(5, manager.DeferredMessageCountTotal()); + Assert.AreEqual(4, manager.DeferredMessageCountTotal()); - Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn)); - Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab)); Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); AddPrefabsToClient(client); diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs index c4997fe..af00165 100644 --- a/Tests/Runtime/DisconnectTests.cs +++ b/Tests/Runtime/DisconnectTests.cs @@ -180,6 +180,9 @@ public IEnumerator ClientPlayerDisconnected([Values] ClientDisconnectType client Assert.IsTrue(m_DisconnectedEvent[m_ServerNetworkManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the server {nameof(NetworkManager)} disconnect event entry!"); Assert.IsTrue(m_DisconnectedEvent.ContainsKey(m_ClientNetworkManagers[0]), $"Could not find the client {nameof(NetworkManager)} disconnect event entry!"); Assert.IsTrue(m_DisconnectedEvent[m_ClientNetworkManagers[0]].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the client {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClientsIds.Count}!"); + Assert.IsTrue(m_ServerNetworkManager.ConnectedClients.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClients.Count}!"); + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsList.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClientsList.Count}!"); } if (m_OwnerPersistence == OwnerPersistence.DestroyWithOwner) diff --git a/Tests/Runtime/HiddenVariableTests.cs b/Tests/Runtime/HiddenVariableTests.cs index f110ed1..94b44cf 100644 --- a/Tests/Runtime/HiddenVariableTests.cs +++ b/Tests/Runtime/HiddenVariableTests.cs @@ -59,13 +59,13 @@ public override void OnNetworkDespawn() public void Changed(int before, int after) { - VerboseDebug($"Value changed from {before} to {after} on {NetworkManager.LocalClientId}"); + VerboseDebug($"[Client-{NetworkManager.LocalClientId}][{name}][MyNetworkVariable] Value changed from {before} to {after}"); ValueOnClient[NetworkManager.LocalClientId] = after; } public void ListChanged(NetworkListEvent listEvent) { - Debug.Log($"ListEvent received: type {listEvent.Type}, index {listEvent.Index}, value {listEvent.Value}"); - Debug.Assert(ExpectedSize == MyNetworkList.Count); + VerboseDebug($"[Client-{NetworkManager.LocalClientId}][{name}][MyNetworkList] ListEvent received: type {listEvent.Type}, index {listEvent.Index}, value {listEvent.Value}"); + Debug.Assert(ExpectedSize == MyNetworkList.Count, $"[{name}] List change failure! Expected Count: {ExpectedSize} Actual Count:{MyNetworkList.Count}"); } } @@ -185,10 +185,13 @@ public IEnumerator HiddenVariableTest() var otherClient = m_ServerNetworkManager.ConnectedClientsList[2]; m_NetSpawnedObject = SpawnObject(m_TestNetworkPrefab, m_ClientNetworkManagers[1]).GetComponent(); - yield return RefreshGameObects(4); + yield return RefreshGameObects(NumberOfClients); // === Check spawn occurred yield return WaitForSpawnCount(NumberOfClients + 1); + + AssertOnTimeout($"Timed out waiting for all clients to spawn {m_NetSpawnedObject.name}"); + Debug.Assert(HiddenVariableObject.SpawnCount == NumberOfClients + 1); VerboseDebug("Objects spawned"); @@ -205,7 +208,6 @@ public IEnumerator HiddenVariableTest() // ==== Hide our object to a different client HiddenVariableObject.ExpectedSize = 2; m_NetSpawnedObject.NetworkHide(otherClient.ClientId); - currentValueSet = 3; m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = currentValueSet; m_NetSpawnedObject.GetComponent().MyNetworkList.Add(currentValueSet); @@ -222,7 +224,7 @@ public IEnumerator HiddenVariableTest() VerboseDebug("Object spawned"); // ==== We need a refresh for the newly re-spawned object - yield return RefreshGameObects(4); + yield return RefreshGameObects(NumberOfClients); currentValueSet = 4; m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = currentValueSet; diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs b/Tests/Runtime/NetworkManagerEventsTests.cs index 195e8b3..8b1c993 100644 --- a/Tests/Runtime/NetworkManagerEventsTests.cs +++ b/Tests/Runtime/NetworkManagerEventsTests.cs @@ -13,6 +13,60 @@ internal class NetworkManagerEventsTests private NetworkManager m_ClientManager; private NetworkManager m_ServerManager; + private NetworkManager m_NetworkManagerInstantiated; + private bool m_Instantiated; + private bool m_Destroyed; + + /// + /// Validates the and event notifications + /// + [UnityTest] + public IEnumerator InstantiatedAndDestroyingNotifications() + { + NetworkManager.OnInstantiated += NetworkManager_OnInstantiated; + NetworkManager.OnDestroying += NetworkManager_OnDestroying; + var waitPeriod = new WaitForSeconds(0.01f); + var prefab = new GameObject("InstantiateDestroy"); + var networkManagerPrefab = prefab.AddComponent(); + + Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} prefab did not get instantiated event notification!"); + Assert.IsTrue(m_NetworkManagerInstantiated == networkManagerPrefab, $"{nameof(NetworkManager)} prefab parameter did not match!"); + + m_Instantiated = false; + m_NetworkManagerInstantiated = null; + + for (int i = 0; i < 3; i++) + { + var instance = Object.Instantiate(prefab); + var networkManager = instance.GetComponent(); + Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} instance-{i} did not get instantiated event notification!"); + Assert.IsTrue(m_NetworkManagerInstantiated == networkManager, $"{nameof(NetworkManager)} instance-{i} parameter did not match!"); + Object.DestroyImmediate(instance); + Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} instance-{i} did not get destroying event notification!"); + m_Instantiated = false; + m_NetworkManagerInstantiated = null; + m_Destroyed = false; + } + m_NetworkManagerInstantiated = networkManagerPrefab; + Object.Destroy(prefab); + yield return null; + Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} prefab did not get destroying event notification!"); + NetworkManager.OnInstantiated -= NetworkManager_OnInstantiated; + NetworkManager.OnDestroying -= NetworkManager_OnDestroying; + } + + private void NetworkManager_OnInstantiated(NetworkManager networkManager) + { + m_Instantiated = true; + m_NetworkManagerInstantiated = networkManager; + } + + private void NetworkManager_OnDestroying(NetworkManager networkManager) + { + m_Destroyed = true; + Assert.True(m_NetworkManagerInstantiated == networkManager, $"Destroying {nameof(NetworkManager)} and current instance is not a match for the one passed into the event!"); + } + [UnityTest] public IEnumerator OnServerStoppedCalledWhenServerStops() { diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs index faf4402..bda9d15 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs @@ -265,6 +265,8 @@ bool WaitForClientsToSpawnNetworkObject() // After the 1st client has been given ownership to the object, this will be used to make sure each previous owner properly received the remove ownership message var previousClientComponent = (NetworkObjectOwnershipComponent)null; + var networkManagersDAMode = new List(); + for (int clientIndex = 0; clientIndex < NumberOfClients; clientIndex++) { clientObject = clientObjects[clientIndex]; @@ -322,6 +324,21 @@ bool WaitForClientsToSpawnNetworkObject() // In distributed authority mode, the current owner just rolls the ownership back over to the DAHost client (i.e. host mocking CMB Service) if (m_DistributedAuthority) { + // In distributed authority, we have to clear out the NetworkManager instances as this changes relative to authority. + networkManagersDAMode.Clear(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + if (clientNetworkManager.LocalClientId == clientObject.OwnerClientId) + { + continue; + } + networkManagersDAMode.Add(clientNetworkManager); + } + + if (!UseCMBService() && clientObject.OwnerClientId != m_ServerNetworkManager.LocalClientId) + { + networkManagersDAMode.Add(m_ServerNetworkManager); + } clientObject.ChangeOwnership(NetworkManager.ServerClientId); } else @@ -330,7 +347,18 @@ bool WaitForClientsToSpawnNetworkObject() } } - yield return WaitForConditionOrTimeOut(ownershipMessageHooks); + if (m_DistributedAuthority) + { + // We use an alternate method (other than message hooks) to verify each client received the ownership message since message hooks becomes problematic when you need + // to make dynamic changes to your targets. + yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllTargetedClients(networkManagersDAMode, clientObject.NetworkObjectId, NetworkManager.ServerClientId)); + } + else + { + yield return WaitForConditionOrTimeOut(ownershipMessageHooks); + } + + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(ChangeOwnershipMessage)} message (back to server)."); Assert.That(serverComponent.OnGainedOwnershipFired); @@ -351,6 +379,22 @@ bool WaitForClientsToSpawnNetworkObject() serverComponent.ResetFlags(); } + private bool OwnershipChangedOnAllTargetedClients(List networkManagers, ulong networkObjectId, ulong expectedOwner) + { + foreach (var networkManager in networkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) + { + return false; + } + if (networkManager.SpawnManager.SpawnedObjects[networkObjectId].OwnerClientId != expectedOwner) + { + return false; + } + } + return true; + } + private const int k_NumberOfSpawnedObjects = 5; private bool AllClientsHaveCorrectObjectCount() diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs index 7c8b137..b80e055 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs @@ -12,8 +12,6 @@ namespace Unity.Netcode.RuntimeTests internal class NetworkObjectSpawnManyObjectsTests : NetcodeIntegrationTest { protected override int NumberOfClients => 1; - // "many" in this case means enough to exceed a ushort_max message size written in the header - // 1500 is not a magic number except that it's big enough to trigger a failure private const int k_SpawnedObjects = 1500; private NetworkPrefab m_PrefabToSpawn; @@ -52,19 +50,23 @@ protected override void OnServerAndClientsCreated() } [UnityTest] - // When this test fails it does so without an exception and will wait the default ~6 minutes - [Timeout(10000)] public IEnumerator WhenManyObjectsAreSpawnedAtOnce_AllAreReceived() { + var timeStarted = Time.realtimeSinceStartup; for (int x = 0; x < k_SpawnedObjects; x++) { NetworkObject serverObject = Object.Instantiate(m_PrefabToSpawn.Prefab).GetComponent(); serverObject.NetworkManagerOwner = m_ServerNetworkManager; serverObject.Spawn(); } + + var timeSpawned = Time.realtimeSinceStartup - timeStarted; + // Provide plenty of time to spawn all 1500 objects in case the CI VM is running slow + var timeoutHelper = new TimeoutHelper(30); // ensure all objects are replicated - yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects); - AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!"); + yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects, timeoutHelper); + + AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects! Time to spawn: {timeSpawned} | Time to timeout: {timeStarted - Time.realtimeSinceStartup}", timeoutHelper); } } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs index 8f65f34..31105ef 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text; using NUnit.Framework; using Unity.Netcode.Components; using Unity.Netcode.TestHelpers.Runtime; @@ -539,5 +540,279 @@ protected override bool OnIsServerAuthoritative() } } } + + [TestFixture(HostOrServer.DAHost, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using distributed authority + [TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Server)] // Validate we have not impacted NetworkTransform server authoritative mode + [TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using client-server + internal class NestedNetworkTransformTests : IntegrationTestWithApproximation + { + private const int k_NestedChildren = 5; + protected override int NumberOfClients => 2; + + private GameObject m_SpawnObject; + + private NetworkTransform.AuthorityModes m_AuthorityMode; + + private StringBuilder m_ErrorLog = new StringBuilder(); + + private List m_NetworkManagers = new List(); + private List m_SpawnedObjects = new List(); + + public NestedNetworkTransformTests(HostOrServer hostOrServer, NetworkTransform.AuthorityModes authorityMode) : base(hostOrServer) + { + m_AuthorityMode = authorityMode; + } + + /// + /// Creates a player prefab with several nested NetworkTransforms + /// + protected override void OnCreatePlayerPrefab() + { + var networkTransform = m_PlayerPrefab.AddComponent(); + networkTransform.AuthorityMode = m_AuthorityMode; + var parent = m_PlayerPrefab; + // Add several nested NetworkTransforms + for (int i = 0; i < k_NestedChildren; i++) + { + var nestedChild = new GameObject(); + nestedChild.transform.parent = parent.transform; + var nestedNetworkTransform = nestedChild.AddComponent(); + nestedNetworkTransform.AuthorityMode = m_AuthorityMode; + nestedNetworkTransform.InLocalSpace = true; + parent = nestedChild; + } + base.OnCreatePlayerPrefab(); + } + + private void RandomizeObjectTransformPositions(GameObject gameObject) + { + var networkObject = gameObject.GetComponent(); + Assert.True(networkObject.ChildNetworkBehaviours.Count > 0); + + foreach (var networkTransform in networkObject.NetworkTransforms) + { + networkTransform.gameObject.transform.position = GetRandomVector3(-15.0f, 15.0f); + } + } + + /// + /// Randomizes each player's position when validating distributed authority + /// + /// + private GameObject FetchLocalPlayerPrefabToSpawn() + { + RandomizeObjectTransformPositions(m_PlayerPrefab); + return m_PlayerPrefab; + } + + /// + /// Randomizes the player position when validating client-server + /// + /// + /// + private void ConnectionApprovalHandler(NetworkManager.ConnectionApprovalRequest connectionApprovalRequest, NetworkManager.ConnectionApprovalResponse connectionApprovalResponse) + { + connectionApprovalResponse.Approved = true; + connectionApprovalResponse.CreatePlayerObject = true; + RandomizeObjectTransformPositions(m_PlayerPrefab); + connectionApprovalResponse.Position = GetRandomVector3(-15.0f, 15.0f); + } + + protected override void OnServerAndClientsCreated() + { + // Create a prefab to spawn with each NetworkManager as the owner + m_SpawnObject = CreateNetworkObjectPrefab("SpawnObj"); + var networkTransform = m_SpawnObject.AddComponent(); + networkTransform.AuthorityMode = m_AuthorityMode; + var parent = m_SpawnObject; + // Add several nested NetworkTransforms + for (int i = 0; i < k_NestedChildren; i++) + { + var nestedChild = new GameObject(); + nestedChild.transform.parent = parent.transform; + var nestedNetworkTransform = nestedChild.AddComponent(); + nestedNetworkTransform.AuthorityMode = m_AuthorityMode; + nestedNetworkTransform.InLocalSpace = true; + parent = nestedChild; + } + + if (m_DistributedAuthority) + { + if (!UseCMBService()) + { + m_ServerNetworkManager.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn; + } + + foreach (var client in m_ClientNetworkManagers) + { + client.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn; + } + } + else + { + m_ServerNetworkManager.NetworkConfig.ConnectionApproval = true; + m_ServerNetworkManager.ConnectionApprovalCallback += ConnectionApprovalHandler; + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.ConnectionApproval = true; + } + } + + base.OnServerAndClientsCreated(); + } + + /// + /// Validates the transform positions of two NetworkObject instances + /// + /// the local instance (source of truth) + /// the remote instance + /// + private bool ValidateTransforms(NetworkObject current, NetworkObject testing) + { + if (current.ChildNetworkBehaviours.Count == 0 || testing.ChildNetworkBehaviours.Count == 0) + { + return false; + } + + for (int i = 0; i < current.NetworkTransforms.Count - 1; i++) + { + var transformA = current.NetworkTransforms[i].transform; + var transformB = testing.NetworkTransforms[i].transform; + if (!Approximately(transformA.position, transformB.position)) + { + m_ErrorLog.AppendLine($"TransformA Position {transformA.position} != TransformB Position {transformB.position}"); + return false; + } + if (!Approximately(transformA.localPosition, transformB.localPosition)) + { + m_ErrorLog.AppendLine($"TransformA Local Position {transformA.position} != TransformB Local Position {transformB.position}"); + return false; + } + if (transformA.parent != null) + { + if (current.NetworkTransforms[i].InLocalSpace != testing.NetworkTransforms[i].InLocalSpace) + { + m_ErrorLog.AppendLine($"NetworkTransform-{current.OwnerClientId}-{current.NetworkTransforms[i].NetworkBehaviourId} InLocalSpace ({current.NetworkTransforms[i].InLocalSpace}) is different from the remote instance version on Client-{testing.NetworkManager.LocalClientId}!"); + return false; + } + } + } + return true; + } + + /// + /// Validates all player instances spawned with the correct positions including all nested NetworkTransforms + /// When running in server authority mode we are validating this fix did not impact that. + /// + private bool AllClientInstancesSynchronized() + { + m_ErrorLog.Clear(); + + foreach (var current in m_NetworkManagers) + { + var currentPlayer = current.LocalClient.PlayerObject; + var currentNetworkObjectId = currentPlayer.NetworkObjectId; + foreach (var testing in m_NetworkManagers) + { + if (currentPlayer == testing.LocalClient.PlayerObject) + { + continue; + } + + if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId)) + { + m_ErrorLog.AppendLine($"Failed to find Client-{currentPlayer.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!"); + return false; + } + + var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId]; + if (!ValidateTransforms(currentPlayer, remoteInstance)) + { + m_ErrorLog.AppendLine($"Failed to validate Client-{currentPlayer.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!"); + return false; + } + } + } + return true; + } + + /// + /// Validates that dynamically spawning works the same. + /// When running in server authority mode we are validating this fix did not impact that. + /// + /// + private bool AllSpawnedObjectsSynchronized() + { + m_ErrorLog.Clear(); + + foreach (var current in m_SpawnedObjects) + { + var currentNetworkObject = current.GetComponent(); + var currentNetworkObjectId = currentNetworkObject.NetworkObjectId; + foreach (var testing in m_NetworkManagers) + { + if (currentNetworkObject.OwnerClientId == testing.LocalClientId) + { + continue; + } + + if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId)) + { + m_ErrorLog.AppendLine($"Failed to find Client-{currentNetworkObject.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!"); + return false; + } + + var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId]; + if (!ValidateTransforms(currentNetworkObject, remoteInstance)) + { + m_ErrorLog.AppendLine($"Failed to validate Client-{currentNetworkObject.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!"); + return false; + } + } + } + return true; + } + + /// + /// Validates that spawning player and dynamically spawned prefab instances with nested NetworkTransforms + /// synchronizes properly in both client-server and distributed authority when using owner authoritative mode. + /// + [UnityTest] + public IEnumerator NestedNetworkTransformSpawnPositionTest() + { + if (!m_DistributedAuthority || (m_DistributedAuthority && !UseCMBService())) + { + m_NetworkManagers.Add(m_ServerNetworkManager); + } + m_NetworkManagers.AddRange(m_ClientNetworkManagers); + + yield return WaitForConditionOrTimeOut(AllClientInstancesSynchronized); + AssertOnTimeout($"Failed to synchronize all client instances!\n{m_ErrorLog}"); + + foreach (var networkManager in m_NetworkManagers) + { + // Randomize the position + RandomizeObjectTransformPositions(m_SpawnObject); + + // Create an instance owned by the specified networkmanager + m_SpawnedObjects.Add(SpawnObject(m_SpawnObject, networkManager)); + } + // Randomize the position once more just to assure we are instantiating remote instances + // with a completely different position + RandomizeObjectTransformPositions(m_SpawnObject); + yield return WaitForConditionOrTimeOut(AllSpawnedObjectsSynchronized); + AssertOnTimeout($"Failed to synchronize all spawned NetworkObject instances!\n{m_ErrorLog}"); + m_SpawnedObjects.Clear(); + m_NetworkManagers.Clear(); + } + + protected override IEnumerator OnTearDown() + { + // In case there was a failure, go ahead and clear these lists out for any pending TextFixture passes + m_SpawnedObjects.Clear(); + m_NetworkManagers.Clear(); + return base.OnTearDown(); + } + } } #endif diff --git a/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs b/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs index 7d7e785..0c8a2ff 100644 --- a/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs +++ b/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs @@ -5,12 +5,14 @@ using System.Text; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; using UnityEngine.TestTools; using Random = UnityEngine.Random; namespace Unity.Netcode.RuntimeTests { /// + /// Client-Server only test /// Validates using managed collections with NetworkVariable. /// Managed Collections Tested: /// - List @@ -18,24 +20,23 @@ namespace Unity.Netcode.RuntimeTests /// - HashSet /// This also does some testing on nested collections, but does /// not test every possible combination. - /// - [TestFixture(HostOrServer.Host, CollectionTypes.List)] - [TestFixture(HostOrServer.Server, CollectionTypes.List)] + /// + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] public class NetworkVariableCollectionsTests : NetcodeIntegrationTest { - public enum CollectionTypes - { - Dictionary, - List, - } - protected override int NumberOfClients => 2; - private CollectionTypes m_CollectionType; + private bool m_EnableDebug; - public NetworkVariableCollectionsTests(HostOrServer hostOrServer, CollectionTypes collectionType) : base(hostOrServer) + public NetworkVariableCollectionsTests(HostOrServer hostOrServer) : base(hostOrServer) { - m_CollectionType = collectionType; + m_EnableDebug = false; + } + + protected override bool OnSetVerboseDebug() + { + return m_EnableDebug; } protected override IEnumerator OnSetup() @@ -50,15 +51,21 @@ protected override IEnumerator OnSetup() return base.OnSetup(); } + private void AddPlayerComponent() where T : ListTestHelperBase + { + var component = m_PlayerPrefab.AddComponent(); + component.SetDebugMode(m_EnableDebug); + } + protected override void OnCreatePlayerPrefab() { - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); + AddPlayerComponent(); + AddPlayerComponent(); + AddPlayerComponent(); + AddPlayerComponent(); + AddPlayerComponent(); + AddPlayerComponent(); + AddPlayerComponent(); base.OnCreatePlayerPrefab(); } @@ -90,6 +97,7 @@ public IEnumerator TestListBuiltInTypeCollections() { /////////////////////////////////////////////////////////////////////////// // List Single dimension list + compInt = client.LocalClient.PlayerObject.GetComponent(); compIntServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances()); @@ -99,16 +107,34 @@ public IEnumerator TestListBuiltInTypeCollections() AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!"); var randomInt = Random.Range(int.MinValue, int.MaxValue); + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + ////////////////////////////////// + // No Write Owner Add Int + compIntServer.Add(randomInt, ListTestHelperBase.Targets.Owner); + } + ////////////////////////////////// // Owner Add int compInt.Add(randomInt, ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + ////////////////////////////////// + // No Write Server Add Int + compInt.Add(randomInt, ListTestHelperBase.Targets.Server); + } + ////////////////////////////////// // Server Add int compIntServer.Add(randomInt, ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + ////////////////////////////////// // Owner Remove int var index = Random.Range(0, compInt.ListCollectionOwner.Value.Count - 1); @@ -131,12 +157,39 @@ public IEnumerator TestListBuiltInTypeCollections() //////////////////////////////////// // Owner Change int var valueIntChange = Random.Range(int.MinValue, int.MaxValue); + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // No Write Server Change int with IsDirty restore + compIntServer.ListCollectionOwner.Value[index] = valueIntChange; + compIntServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server change failed to restore on {nameof(ListTestHelperInt)} {compInt.name}!"); + + // No Write Server Change int with owner state update override + compIntServer.ListCollectionOwner.Value[index] = valueIntChange; + } compInt.ListCollectionOwner.Value[index] = valueIntChange; compInt.ListCollectionOwner.CheckDirtyState(); yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + ////////////////////////////////// // Server Change int + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // No Write Client Change int with IsDirty restore + compInt.ListCollectionServer.Value[index] = valueIntChange; + compInt.ListCollectionServer.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client-{client.LocalClientId} change failed to restore on {nameof(ListTestHelperInt)} {compInt.name}!"); + + // No Write Client Change int with owner state update override + compInt.ListCollectionServer.Value[index] = valueIntChange; + } compIntServer.ListCollectionServer.Value[index] = valueIntChange; compIntServer.ListCollectionServer.CheckDirtyState(); yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); @@ -211,13 +264,36 @@ public IEnumerator TestListBuiltInTypeCollections() ////////////////////////////////// // Owner Remove List item index = Random.Range(0, compListInt.ListCollectionOwner.Value.Count - 1); + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + compListIntServer.ListCollectionOwner.Value.Remove(compListIntServer.ListCollectionOwner.Value[index]); + compListIntServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server remove failed to restore on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + // No Write Server Remove List item with update restore + compListIntServer.ListCollectionOwner.Value.Remove(compListIntServer.ListCollectionOwner.Value[index]); + } compListInt.Remove(compListInt.ListCollectionOwner.Value[index], ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + ////////////////////////////////// // Server Remove List item index = Random.Range(0, compListIntServer.ListCollectionServer.Value.Count - 1); - compListIntServer.Remove(compListIntServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Owner); + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // No Write Client Remove List item with CheckDirtyState restore + compListInt.Remove(compListInt.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client-{client.LocalClientId} remove failed to restore on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + // No Write Client Remove List item with update restore + compListInt.Remove(compListInt.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server); + } + compListIntServer.Remove(compListIntServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); @@ -370,12 +446,37 @@ public IEnumerator TestListSerializableObjectCollections() //////////////////////////////////// // Owner Change SerializableObject + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // No Write Server Remove Serializable item with IsDirty restore + compObjectServer.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject(); + compObjectServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server change failed to restore on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + // No Write Server Remove Serializable item with owner state update restore + compObjectServer.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject(); + } compObject.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject(); compObject.ListCollectionOwner.CheckDirtyState(); yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); ////////////////////////////////// // Server Change SerializableObject + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // No Write Client Remove Serializable item with IsDirty restore + compObject.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject(); + compObject.ListCollectionServer.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client-{client.LocalClientId} change failed to restore on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + // No Write Client Remove Serializable item with owner state update restore + compObject.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject(); + } compObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject(); compObjectServer.ListCollectionServer.CheckDirtyState(); yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); @@ -427,7 +528,7 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!"); /////////////////////////////////////////////////////////////////////////// - // List> Nested List Validation + // List> Nested List Validation compListObject = client.LocalClient.PlayerObject.GetComponent(); compListObjectServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances()); @@ -437,24 +538,24 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}"); ////////////////////////////////// - // Owner Add List item + // Owner Add List item compListObject.Add(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}"); ////////////////////////////////// - // Server Add List item + // Server Add List item compListObjectServer.Add(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}"); ////////////////////////////////// - // Owner Remove List item + // Owner Remove List item index = Random.Range(0, compListObject.ListCollectionOwner.Value.Count - 1); compListObject.Remove(compListObject.ListCollectionOwner.Value[index], ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}"); ////////////////////////////////// - // Server Remove List item + // Server Remove List item index = Random.Range(0, compListObjectServer.ListCollectionServer.Value.Count - 1); compListObjectServer.Remove(compListObjectServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); @@ -468,7 +569,7 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}"); //////////////////////////////////// - // Owner Change List item + // Owner Change List item index = Random.Range(0, compListObject.ListCollectionOwner.Value.Count - 1); compListObject.ListCollectionOwner.Value[index] = SerializableObject.GetListOfRandomObjects(5); compListObject.ListCollectionOwner.CheckDirtyState(); @@ -477,7 +578,7 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"Client-{client.LocalClientId} change index ({index}) failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}"); ////////////////////////////////// - // Server Change List item + // Server Change List item index = Random.Range(0, compListObjectServer.ListCollectionServer.Value.Count - 1); compListObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetListOfRandomObjects(5); compListObjectServer.ListCollectionServer.CheckDirtyState(); @@ -486,12 +587,12 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}"); //////////////////////////////////// - // Owner Add Range of List items + // Owner Add Range of List items compListObject.AddRange(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} add range failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}"); ////////////////////////////////// - // Server Add Range of List items + // Server Add Range of List items compListObjectServer.AddRange(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server add range failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}"); @@ -503,23 +604,46 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match!"); //////////////////////////////////// - // Owner Full Set List> + // Owner Full Set List> compListObject.FullSet(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!"); ////////////////////////////////// - // Server Full Set List> + // Server Full Set List> compListObjectServer.FullSet(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server full set failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}!"); //////////////////////////////////// - // Owner Clear List> + // Owner Clear List> + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Server Clear List> with IsDirty restore + compListObjectServer.ListCollectionOwner.Value.Clear(); + compListObjectServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server clear owner collection failed to restore back to last known valid state on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!"); + // Server Clear List> with update state restore + compListObjectServer.ListCollectionOwner.Value.Clear(); + } compListObject.Clear(ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!"); ////////////////////////////////// - // Server Clear List> + // Server Clear List> + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Client Clear List> with IsDirty restore + compListObject.ListCollectionServer.Value.Clear(); + compListObject.ListCollectionServer.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client clear owner collection failed to restore back to last known valid state on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!"); + + // Client Clear List> with update state restore + compListObject.ListCollectionServer.Value.Clear(); + } compListObjectServer.Clear(ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}!"); @@ -539,6 +663,111 @@ private int GetNextKey() return m_CurrentKey; } + private int m_Stage; + + private List m_Clients; + + private bool m_IsInitialized = false; + private StringBuilder m_InitializedStatus = new StringBuilder(); + + private IEnumerator ValidateClients(NetworkManager clientBeingTested, bool initialize = false) + { + VerboseDebug($">>>>>>>>>>>>>>>>>>>>>>>>>[Client-{clientBeingTested.LocalClientId}][{m_Stage}][Validation]<<<<<<<<<<<<<<<<<<<<<<<<< "); + m_Stage++; + var compDictionary = (DictionaryTestHelper)null; + var compDictionaryServer = (DictionaryTestHelper)null; + var className = $"{nameof(DictionaryTestHelper)}"; + var clientsInitialized = new Dictionary(); + + var validateTimeout = new TimeoutHelper(0.25f); + + foreach (var client in m_Clients) + { + var ownerInitialized = false; + var serverInitialized = false; + /////////////////////////////////////////////////////////////////////////// + // Dictionary> nested dictionaries + compDictionary = client.LocalClient.PlayerObject.GetComponent(); + compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances(), validateTimeout); + if (initialize) + { + if (validateTimeout.HasTimedOut()) + { + m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Owner] Failed validation: {compDictionary.GetLog()}"); + } + else + { + m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Owner] Passed validation!"); + } + ownerInitialized = !validateTimeout.HasTimedOut(); + } + else + { + AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}"); + } + + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances(), validateTimeout); + if (initialize) + { + if (validateTimeout.HasTimedOut()) + { + m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Server] Failed validation: {compDictionaryServer.GetLog()}"); + } + else + { + m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Server] Passed validation!"); + } + serverInitialized = !validateTimeout.HasTimedOut(); + } + else + { + AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}"); + } + + if (initialize) + { + clientsInitialized.Add(client.LocalClientId, ownerInitialized & serverInitialized); + } + } + + if (initialize) + { + m_IsInitialized = true; + foreach (var entry in clientsInitialized) + { + if (!entry.Value) + { + m_IsInitialized = false; + break; + } + } + } + } + + private void ValidateClientsFlat(NetworkManager clientBeingTested) + { + if (!m_EnableDebug) + { + return; + } + VerboseDebug($">>>>>>>>>>>>>>>>>>>>>>>>>[{clientBeingTested.name}][{m_Stage}][Validation]<<<<<<<<<<<<<<<<<<<<<<<<< "); + m_Stage++; + var compDictionary = (DictionaryTestHelper)null; + var compDictionaryServer = (DictionaryTestHelper)null; + var className = $"{nameof(DictionaryTestHelper)}"; + foreach (var client in m_Clients) + { + /////////////////////////////////////////////////////////////////////////// + // Dictionary> nested dictionaries + compDictionary = client.LocalClient.PlayerObject.GetComponent(); + compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + Assert.True(compDictionary.ValidateInstances(), $"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}"); + Assert.True(compDictionaryServer.ValidateInstances(), $"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}"); + } + } + + [UnityTest] public IEnumerator TestDictionaryCollections() { @@ -546,15 +775,47 @@ public IEnumerator TestDictionaryCollections() var compDictionaryServer = (DictionaryTestHelper)null; var className = $"{nameof(DictionaryTestHelper)}"; - var clientList = m_ClientNetworkManagers.ToList(); + m_Clients = m_ClientNetworkManagers.ToList(); if (m_ServerNetworkManager.IsHost) { - clientList.Insert(0, m_ServerNetworkManager); + m_Clients.Insert(0, m_ServerNetworkManager); } m_CurrentKey = 1000; - foreach (var client in clientList) + if (m_EnableDebug) + { + VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Init Values <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + foreach (var client in m_Clients) + { + compDictionary = client.LocalClient.PlayerObject.GetComponent(); + compDictionary.InitValues(); + compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + compDictionaryServer.InitValues(); + } + VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Init Check <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + var count = 0; + while (count < 3) + { + m_InitializedStatus.Clear(); + foreach (var client in m_Clients) + { + yield return ValidateClients(client, true); + } + if (m_IsInitialized) + { + break; + } + count++; + m_Stage = 0; + } + + Assert.IsTrue(m_IsInitialized, $"Not all clients synchronized properly!\n {m_InitializedStatus.ToString()}"); + VerboseDebug(m_InitializedStatus.ToString()); + } + + VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BEGIN <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + foreach (var client in m_Clients) { /////////////////////////////////////////////////////////////////////////// // Dictionary> nested dictionaries @@ -562,18 +823,55 @@ public IEnumerator TestDictionaryCollections() compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances()); AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}"); - yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances()); AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}"); ////////////////////////////////// // Owner Add SerializableObject Entry - compDictionary.Add((GetNextKey(), SerializableObject.GetRandomObject()), ListTestHelperBase.Targets.Owner); + var newEntry = (GetNextKey(), SerializableObject.GetRandomObject()); + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Server-side add same key and SerializableObject prior to being added to the owner side + compDictionaryServer.ListCollectionOwner.Value.Add(newEntry.Item1, newEntry.Item2); + // Checking if dirty on server side should revert back to origina known current dictionary state + compDictionaryServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server add to owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + // Server-side add the same key and SerializableObject to owner write permission (would throw key exists exception too if previous failed) + compDictionaryServer.ListCollectionOwner.Value.Add(newEntry.Item1, newEntry.Item2); + // Server-side add a completely new key and SerializableObject to to owner write permission property + compDictionaryServer.ListCollectionOwner.Value.Add(GetNextKey(), SerializableObject.GetRandomObject()); + // Both should be overridden by the owner-side update + + } + VerboseDebug($"[{compDictionary.name}][Owner] Adding Key: {newEntry.Item1}"); + // Add key and SerializableObject to owner side + compDictionary.Add(newEntry, ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + + ValidateClientsFlat(client); ////////////////////////////////// // Server Add SerializableObject Entry - compDictionaryServer.Add((GetNextKey(), SerializableObject.GetRandomObject()), ListTestHelperBase.Targets.Server); + newEntry = (GetNextKey(), SerializableObject.GetRandomObject()); + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Client-side add same key and SerializableObject to server write permission property + compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2); + // Checking if dirty on client side should revert back to origina known current dictionary state + compDictionary.ListCollectionServer.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client-{client.LocalClientId} add to server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + // Client-side add the same key and SerializableObject to server write permission property (would throw key exists exception too if previous failed) + compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2); + // Client-side add a completely new key and SerializableObject to to server write permission property + compDictionary.ListCollectionServer.Value.Add(GetNextKey(), SerializableObject.GetRandomObject()); + // Both should be overridden by the server-side update + } + VerboseDebug($"[{compDictionaryServer.name}][Server] Adding Key: {newEntry.Item1}"); + compDictionaryServer.Add(newEntry, ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server add failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); ////////////////////////////////// @@ -583,10 +881,11 @@ public IEnumerator TestDictionaryCollections() compDictionary.Remove(valueInt, ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + ////////////////////////////////// // Server Remove SerializableObject Entry - index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); - valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; + index = Random.Range(0, compDictionary.ListCollectionServer.Value.Keys.Count - 1); + valueInt = compDictionary.ListCollectionServer.Value.Keys.ToList()[index]; compDictionaryServer.Remove(valueInt, ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server remove failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); @@ -597,41 +896,101 @@ public IEnumerator TestDictionaryCollections() yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances()); AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}"); + ValidateClientsFlat(client); //////////////////////////////////// // Owner Change SerializableObject Entry - index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); - valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; - compDictionary.ListCollectionOwner.Value[valueInt] = SerializableObject.GetRandomObject(); - compDictionary.ListCollectionOwner.CheckDirtyState(); - yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); - AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + var randomObject = SerializableObject.GetRandomObject(); + if (compDictionary.ListCollectionOwner.Value.Keys.Count != 0) + { + if (compDictionary.ListCollectionOwner.Value.Keys.Count == 1) + { + index = 0; + valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[0]; + } + else + { + index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); + valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; + } + + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Server-side update same key value prior to being updated to the owner side + compDictionaryServer.ListCollectionOwner.Value[valueInt] = randomObject; + // Checking if dirty on server side should revert back to origina known current dictionary state + compDictionaryServer.ListCollectionOwner.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Server update collection entry value to local owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + + // Server-side update same key but with different value prior to being updated to the owner side + compDictionaryServer.ListCollectionOwner.Value[valueInt] = SerializableObject.GetRandomObject(); + if (compDictionaryServer.ListCollectionOwner.Value.Keys.Count > 1) + { + // Server-side update different key with different value prior to being updated to the owner side + compDictionaryServer.ListCollectionOwner.Value[compDictionaryServer.ListCollectionOwner.Value.Keys.ToList()[(index + 1) % compDictionaryServer.ListCollectionOwner.Value.Keys.Count]] = SerializableObject.GetRandomObject(); + } + // Owner-side update should force restore to current known value before updating to the owner's state update of the original index and SerializableObject + } + + compDictionary.ListCollectionOwner.Value[valueInt] = randomObject; + compDictionary.ListCollectionOwner.CheckDirtyState(); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + } + ////////////////////////////////// // Server Change SerializableObject - index = Random.Range(0, compDictionaryServer.ListCollectionOwner.Value.Keys.Count - 1); - valueInt = compDictionaryServer.ListCollectionOwner.Value.Keys.ToList()[index]; - compDictionaryServer.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject(); - compDictionaryServer.ListCollectionServer.CheckDirtyState(); - yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); - AssertOnTimeout($"Server change failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + if (compDictionaryServer.ListCollectionServer.Value.Keys.Count != 0) + { + if (compDictionaryServer.ListCollectionServer.Value.Keys.Count == 1) + { + index = 0; + valueInt = compDictionaryServer.ListCollectionServer.Value.Keys.ToList()[0]; + } + else + { + index = Random.Range(0, compDictionaryServer.ListCollectionServer.Value.Keys.Count - 1); + valueInt = compDictionaryServer.ListCollectionServer.Value.Keys.ToList()[index]; + } - //////////////////////////////////// - // Owner Full Set Dictionary - compDictionary.FullSet(DictionaryTestHelper.GetDictionaryValues(), ListTestHelperBase.Targets.Owner); - yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); - AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); - ////////////////////////////////// - // Server Full Set Dictionary - compDictionaryServer.FullSet(DictionaryTestHelper.GetDictionaryValues(), ListTestHelperBase.Targets.Server); - yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); - AssertOnTimeout($"Server full set failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + // Only test restore on non-host clients (otherwise a host is both server and client/owner) + if (!client.IsServer) + { + // Owner-side update same key value prior to being updated to the server side + compDictionary.ListCollectionServer.Value[valueInt] = randomObject; + // Checking if dirty on owner side should revert back to origina known current dictionary state + compDictionary.ListCollectionServer.IsDirty(); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Client-{client.LocalClientId} update collection entry value to local server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + + // Owner-side update same key but with different value prior to being updated to the server side + compDictionary.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject(); + + if (compDictionary.ListCollectionServer.Value.Keys.Count > 1) + { + // Owner-side update different key with different value prior to being updated to the server side + compDictionary.ListCollectionServer.Value[compDictionary.ListCollectionServer.Value.Keys.ToList()[(index + 1) % compDictionary.ListCollectionServer.Value.Keys.Count]] = SerializableObject.GetRandomObject(); + } + // Server-side update should force restore to current known value before updating to the server's state update of the original index and SerializableObject + } + + compDictionaryServer.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject(); + compDictionaryServer.ListCollectionServer.CheckDirtyState(); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + } + ValidateClientsFlat(client); //////////////////////////////////// // Owner Clear compDictionary.Clear(ListTestHelperBase.Targets.Owner); + VerboseDebug($"[{compDictionary.name}] Clearing dictionary.."); yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); ////////////////////////////////// // Server Clear + VerboseDebug($"[{compDictionaryServer.name}] Clearing dictionary.."); compDictionaryServer.Clear(ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server clear failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); @@ -641,6 +1000,22 @@ public IEnumerator TestDictionaryCollections() yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances()); AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}"); + + //////////////////////////////////// + // Owner Full Set Dictionary + compDictionary.FullSet(DictionaryTestHelper.GetDictionaryValues(), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + ////////////////////////////////// + // Server Full Set Dictionary + compDictionaryServer.FullSet(DictionaryTestHelper.GetDictionaryValues(), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + if (m_EnableDebug) + { + yield return ValidateClients(client); + m_Stage = 0; + } } } @@ -837,6 +1212,505 @@ public IEnumerator TestHashSetBuiltInTypeCollections() } } + [TestFixture(HostOrServer.DAHost, CollectionTypes.List)] + [TestFixture(HostOrServer.DAHost, CollectionTypes.Dictionary)] + [TestFixture(HostOrServer.Host, CollectionTypes.List)] + [TestFixture(HostOrServer.Host, CollectionTypes.Dictionary)] + [TestFixture(HostOrServer.Server, CollectionTypes.List)] + [TestFixture(HostOrServer.Server, CollectionTypes.Dictionary)] + public class NetworkVariableCollectionsChangingTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + public enum CollectionTypes + { + Dictionary, + List, + } + private StringBuilder m_ErrorLog = new StringBuilder(); + private CollectionTypes m_CollectionType; + private GameObject m_TestPrefab; + private NetworkObject m_Instance; + + public NetworkVariableCollectionsChangingTests(HostOrServer hostOrServer, CollectionTypes collectionType) : base(hostOrServer) + { + m_CollectionType = collectionType; + } + + protected override void OnServerAndClientsCreated() + { + m_TestPrefab = CreateNetworkObjectPrefab("TestObject"); + if (m_CollectionType == CollectionTypes.Dictionary) + { + m_TestPrefab.AddComponent(); + } + else + { + m_TestPrefab.AddComponent(); + } + if (m_DistributedAuthority) + { + var networkObject = m_TestPrefab.GetComponent(); + networkObject.SetOwnershipStatus(NetworkObject.OwnershipStatus.Transferable); + } + base.OnServerAndClientsCreated(); + } + + private bool AllInstancesSpawned() + { + if (!UseCMBService()) + { + if (!m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId)) + { + return false; + } + } + + foreach (var client in m_ClientNetworkManagers) + { + if (!client.SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId)) + { + return false; + } + } + return true; + } + + private Dictionary m_NetworkManagers = new Dictionary(); + + private bool ValidateAllInstances() + { + if (!m_NetworkManagers.ContainsKey(m_Instance.OwnerClientId)) + { + return false; + } + + if (!m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId)) + { + return false; + } + + var ownerNetworkManager = m_NetworkManagers[m_Instance.OwnerClientId]; + + var ownerClientInstance = m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent(); + + foreach (var client in m_NetworkManagers) + { + if (client.Value == ownerNetworkManager) + { + continue; + } + + var otherInstance = client.Value.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent(); + if (!ownerClientInstance.ValidateAgainst(otherInstance)) + { + return false; + } + } + return true; + } + + private bool OwnershipChangedOnAllClients(ulong expectedOwner) + { + m_ErrorLog.Clear(); + foreach (var client in m_NetworkManagers) + { + var otherInstance = client.Value.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent(); + if (otherInstance.OwnerClientId != expectedOwner) + { + m_ErrorLog.AppendLine($"Client-{client.Value.LocalClientId} instance of {m_Instance.name} still shows the owner is Client-{otherInstance.OwnerClientId} when it should be Client-{expectedOwner}!"); + return false; + } + } + return true; + } + + private BaseCollectionUpdateHelper GetOwnerInstance() + { + var ownerNetworkManager = m_NetworkManagers[m_Instance.OwnerClientId]; + return m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent(); + } + + /// + /// Gets the authority instance. + /// Client-Server: will always return the server-side instance + /// Distributed Authority: will always return the owner + /// + /// authority instance + private BaseCollectionUpdateHelper GetAuthorityInstance() + { + if (m_DistributedAuthority) + { + return GetOwnerInstance(); + } + else + { + return m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent(); + } + } + + [UnityTest] + public IEnumerator CollectionAndOwnershipChangingTest() + { + BaseCollectionUpdateHelper.VerboseMode = m_EnableVerboseDebug; + var runWaitPeriod = new WaitForSeconds(0.5f); + m_NetworkManagers.Clear(); + if (!UseCMBService() && m_UseHost) + { + m_NetworkManagers.Add(m_ServerNetworkManager.LocalClientId, m_ServerNetworkManager); + } + foreach (var client in m_ClientNetworkManagers) + { + m_NetworkManagers.Add(client.LocalClientId, client); + } + + var authorityNetworkManager = UseCMBService() || !m_UseHost ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + + var instance = SpawnObject(m_TestPrefab, authorityNetworkManager); + m_Instance = instance.GetComponent(); + var helper = instance.GetComponent(); + var currentOwner = helper.OwnerClientId; + yield return WaitForConditionOrTimeOut(AllInstancesSpawned); + AssertOnTimeout($"[Pre][1st Phase] Timed out waiting for all clients to spawn {m_Instance.name}!"); + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start); + yield return runWaitPeriod; + + // Update values, validate values, change owner, updates values, and repeat until all clients have been the owner at least once + for (int i = 0; i < 4; i++) + { + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause); + yield return WaitForConditionOrTimeOut(ValidateAllInstances); + AssertOnTimeout($"[1st Phase] Timed out waiting for all clients to validdate their values!"); + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start); + yield return s_DefaultWaitForTick; + + currentOwner = GetAuthorityInstance().ChangeOwner(); + Assert.IsFalse(currentOwner == ulong.MaxValue, "A non-authority instance attempted to change ownership!"); + + yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllClients(currentOwner)); + AssertOnTimeout($"[1st Phase] Timed out waiting for all clients to change ownership!\n {m_ErrorLog.ToString()}"); + helper = GetOwnerInstance(); + yield return runWaitPeriod; + } + + // Now reset the values + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause); + helper.Clear(); + + // Validate all instances are reset + yield return WaitForConditionOrTimeOut(ValidateAllInstances); + AssertOnTimeout($"[Pre][2nd Phase]Timed out waiting for all clients to validdate their values!"); + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start); + + // Update, change ownership, and repeat until all clients have been the owner at least once + for (int i = 0; i < 4; i++) + { + yield return runWaitPeriod; + currentOwner = GetAuthorityInstance().ChangeOwner(); + Assert.IsFalse(currentOwner == ulong.MaxValue, "A non-authority instance attempted to change ownership!"); + yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllClients(currentOwner)); + AssertOnTimeout($"[2nd Phase] Timed out waiting for all clients to change ownership!"); + helper = GetOwnerInstance(); + } + + helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause); + yield return WaitForConditionOrTimeOut(ValidateAllInstances); + AssertOnTimeout($"[Last Validate] Timed out waiting for all clients to validdate their values!"); + } + } + + #region COLLECTION CHANGING COMPONENTS + /// + /// Helper class to test adding dictionary entries rapidly with frequent ownership changes. + /// This includes a companion integer that is continually incremented and used as the key value for each entry. + /// + public class DictionaryCollectionUpdateHelper : BaseCollectionUpdateHelper + { + private NetworkVariable> m_DictionaryCollection = new NetworkVariable>(new Dictionary(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + private NetworkVariable m_CurrentKeyValue = new NetworkVariable(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + protected override bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper) + { + var otherListHelper = otherHelper as DictionaryCollectionUpdateHelper; + var localValues = m_DictionaryCollection.Value; + var otherValues = otherListHelper.m_DictionaryCollection.Value; + + if (localValues.Count != otherValues.Count) + { + return false; + } + + foreach (var entry in m_DictionaryCollection.Value) + { + if (!otherValues.ContainsKey(entry.Key)) + { + return false; + } + + if (entry.Value != otherValues[entry.Key]) + { + return false; + } + } + return true; + } + protected override void OnClear() + { + m_DictionaryCollection.Value.Clear(); + m_DictionaryCollection.CheckDirtyState(); + base.OnClear(); + } + + protected override void AddItem() + { + m_DictionaryCollection.Value.Add(m_CurrentKeyValue.Value, m_CurrentKeyValue.Value); + m_DictionaryCollection.CheckDirtyState(); + m_CurrentKeyValue.Value++; + } + } + + /// + /// Helper class to test adding list entries rapidly with frequent ownership changes + /// + public class ListCollectionUpdateHelper : BaseCollectionUpdateHelper + { + private NetworkVariable> m_ListCollection = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + + protected override bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper) + { + var otherListHelper = otherHelper as ListCollectionUpdateHelper; + var localValues = m_ListCollection.Value; + var otherValues = otherListHelper.m_ListCollection.Value; + + if (localValues.Count != otherValues.Count) + { + return false; + } + + for (int i = 0; i < localValues.Count - 1; i++) + { + if (localValues[i] != i) + { + return false; + } + + if (localValues[i] != otherValues[i]) + { + return false; + } + } + return true; + } + + protected override void OnClear() + { + m_ListCollection.Value.Clear(); + m_ListCollection.CheckDirtyState(); + base.OnClear(); + } + + protected override void AddItem() + { + m_ListCollection.Value.Add(m_ListCollection.Value.Count); + m_ListCollection.CheckDirtyState(); + } + } + + /// + /// The base class to test rapidly adding items to a collection type + /// + public class BaseCollectionUpdateHelper : NetworkBehaviour + { + public static bool VerboseMode; + private const int k_OwnershipTickDelay = 1; + + public enum HelperStates + { + Stop, + Start, + Pause, + ClearToChangeOwner, + ChangingOwner + } + public HelperStates HelperState { get; private set; } + + private int m_SendClearForOwnershipOnTick; + private ulong m_NextClient = 0; + private ulong m_ClientToSendClear = 0; + + public void SetState(HelperStates helperState) + { + HelperState = helperState; + } + + protected virtual bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper) + { + return true; + } + + public bool ValidateAgainst(BaseCollectionUpdateHelper otherHelper) + { + return OnValidateAgainst(otherHelper); + } + + public override void OnNetworkSpawn() + { + // Register for tick updates + NetworkManager.NetworkTickSystem.Tick += OnNetworkTick; + + base.OnNetworkSpawn(); + } + public override void OnNetworkDespawn() + { + NetworkManager.NetworkTickSystem.Tick -= OnNetworkTick; + base.OnNetworkDespawn(); + } + + protected virtual void OnClear() + { + } + + public void Clear() + { + OnClear(); + } + + protected virtual void AddItem() + { + } + + private bool CanUpdate() + { + return HelperState == HelperStates.Start; + } + + private void Update() + { + // Exit early if not spawn, updating is not enabled, or is not the owner + if (!IsSpawned || !CanUpdate() || !IsOwner) + { + return; + } + + AddItem(); + } + + protected override void OnOwnershipChanged(ulong previous, ulong current) + { + // When the ownership changes and the client is the owner, then immediately add an item to the collection + if (NetworkManager.LocalClientId == current) + { + AddItem(); + } + base.OnOwnershipChanged(previous, current); + } + + + /// + /// Sets the tick delay period of time to provide all in-flight deltas to be processed. + /// + private void SetTickDelay() + { + m_SendClearForOwnershipOnTick = NetworkManager.ServerTime.Tick + k_OwnershipTickDelay; + } + + /// + /// Changes the ownership + /// + /// next owner or ulong.MaxValue that means the authority did not invoke this method + public ulong ChangeOwner() + { + if (HasAuthority && !IsOwnershipChanging()) + { + var index = NetworkManager.ConnectedClientsIds.ToList().IndexOf(OwnerClientId); + index++; + index = index % NetworkManager.ConnectedClientsIds.Count; + m_NextClient = NetworkManager.ConnectedClientsIds[index]; + + // If we are in distributed authority and the authority or we are in client-server and the server, then make the change ourselves. + if (OwnerClientId == NetworkManager.LocalClientId && (NetworkManager.DistributedAuthorityMode || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer))) + { + HelperState = HelperStates.ChangingOwner; + SetTickDelay(); + Log($"Locally changing ownership to Client-{m_NextClient}"); + } + + if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && OwnerClientId != NetworkManager.LocalClientId) + { + // If we are transitioning between a client to the host or client to client, + // send a "heads-up" Rpc to the client prior to changing ownership. The client + // will stop updating for the tick delay period and then send a confirmation + // to the host that it is clear to change ownership. + ChangingOwnershipRpc(RpcTarget.Single(OwnerClientId, RpcTargetUse.Temp)); + Log($"Remotely changing ownership to Client-{m_NextClient}"); + } + + return m_NextClient; + } + + return ulong.MaxValue; + } + + /// + /// Sent by the host to a client when ownership is transitioning from a client to + /// the host or to another client. + /// + [Rpc(SendTo.SpecifiedInParams)] + private void ChangingOwnershipRpc(RpcParams rpcParams = default) + { + // The sender is who we respond to that it is clear to change ownership + m_ClientToSendClear = rpcParams.Receive.SenderClientId; + HelperState = HelperStates.ClearToChangeOwner; + SetTickDelay(); + } + + /// + /// Notification that the current owner has stopped updating and ownership + /// updates can occur without missed updates. + /// + /// + [Rpc(SendTo.SpecifiedInParams)] + private void ChangingOwnershipClearRpc(RpcParams rpcParams = default) + { + HelperState = HelperStates.ChangingOwner; + SetTickDelay(); + Log($"Changing ownership to Client-{m_NextClient} based on ready request."); + } + + private bool IsOwnershipChanging() + { + return HelperState == HelperStates.ClearToChangeOwner || HelperState == HelperStates.ChangingOwner; + } + + private void OnNetworkTick() + { + if (!IsSpawned || !IsOwnershipChanging() || m_SendClearForOwnershipOnTick > NetworkManager.ServerTime.Tick) + { + return; + } + + if (HelperState == HelperStates.ChangingOwner) + { + NetworkObject.ChangeOwnership(m_NextClient); + Log($"Local Change ownership to Client-{m_NextClient} complete! New Owner is {NetworkObject.OwnerClientId} | Expected {m_NextClient}"); + } + else + { + ChangingOwnershipClearRpc(RpcTarget.Single(m_ClientToSendClear, RpcTargetUse.Temp)); + } + HelperState = HelperStates.Stop; + } + + protected void Log(string msg) + { + if (VerboseMode) + { + Debug.Log($"[Client-{NetworkManager.LocalClientId}] {msg}"); + } + } + } + #endregion + #region HASHSET COMPONENT HELPERS public class HashSetBaseTypeTestHelper : ListTestHelperBase, IHashSetTestHelperBase { @@ -1649,6 +2523,14 @@ protected override void OnNetworkPostSpawn() ListCollectionServer.OnValueChanged += OnServerListValuesChanged; ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + if (!IsDebugMode) + { + InitValues(); + } + } + + public void InitValues() + { if (IsServer) { ListCollectionServer.Value = OnSetServerValues(); @@ -1660,8 +2542,8 @@ protected override void OnNetworkPostSpawn() ListCollectionOwner.Value = OnSetOwnerValues(); ListCollectionOwner.CheckDirtyState(); } - base.OnNetworkPostSpawn(); } + public override void OnNetworkDespawn() { ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; @@ -1705,12 +2587,15 @@ public static List> GetListOfListOfRandomObjects(int nu return list; } - - public int IntValue; public long LongValue; public float FloatValue; + public override string ToString() + { + return $"{IntValue},{LongValue},{FloatValue}"; + } + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref IntValue); @@ -2602,7 +3487,6 @@ public static void ResetState() Instances.Clear(); } - public NetworkVariable> ListCollectionServer = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); public NetworkVariable> ListCollectionOwner = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); // This tracks what has changed per instance which is used to compare to all other instances @@ -2865,6 +3749,8 @@ public override void OnNetworkDespawn() #region BASE TEST COMPONENT HELPERS public class ListTestHelperBase : NetworkBehaviour { + protected static bool IsDebugMode { get; private set; } + public enum Targets { Server, @@ -2897,6 +3783,10 @@ protected void LogStart() m_StringBuilder.AppendLine($"[Client-{NetworkManager.LocalClientId}][{name}] Log Started."); } + public void SetDebugMode(bool isDebug) + { + IsDebugMode = isDebug; + } public virtual bool CompareTrackedChanges(Targets target) { diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs index 9ec32bc..601a1ba 100644 --- a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs +++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs @@ -1,5 +1,7 @@ #if COM_UNITY_MODULES_PHYSICS using System.Collections; +using System.Collections.Generic; +using System.Text; using NUnit.Framework; using Unity.Netcode.Components; using Unity.Netcode.TestHelpers.Runtime; @@ -108,5 +110,386 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() Assert.IsTrue(clientPlayerInstance == null, $"[Client-Side] Player {nameof(NetworkObject)} is not null!"); } } + + internal class ContactEventTransformHelperWithInfo : ContactEventTransformHelper, IContactEventHandlerWithInfo + { + public ContactEventHandlerInfo GetContactEventHandlerInfo() + { + var contactEventHandlerInfo = new ContactEventHandlerInfo() + { + HasContactEventPriority = IsOwner, + ProvideNonRigidBodyContactEvents = m_EnableNonRigidbodyContacts.Value, + }; + return contactEventHandlerInfo; + } + + protected override void OnRegisterForContactEvents(bool isRegistering) + { + RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); + } + } + + + internal class ContactEventTransformHelper : NetworkTransform, IContactEventHandler + { + public static Vector3 SessionOwnerSpawnPoint; + public static Vector3 ClientSpawnPoint; + public static bool VerboseDebug; + public enum HelperStates + { + None, + MoveForward, + } + + private HelperStates m_HelperState; + + public void SetHelperState(HelperStates state) + { + m_HelperState = state; + if (!m_NetworkRigidbody.IsKinematic()) + { + m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; + m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; + } + m_NetworkRigidbody.Rigidbody.isKinematic = m_HelperState == HelperStates.None; + if (!m_NetworkRigidbody.IsKinematic()) + { + m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; + m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; + } + + } + + protected struct ContactEventInfo + { + public ulong EventId; + public Vector3 AveragedCollisionNormal; + public Rigidbody CollidingBody; + public Vector3 ContactPoint; + } + + protected List m_ContactEvents = new List(); + + protected NetworkVariable m_EnableNonRigidbodyContacts = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + protected NetworkRigidbody m_NetworkRigidbody; + public ContactEventTransformHelper Target; + + public bool HasContactEvents() + { + return m_ContactEvents.Count > 0; + } + + public Rigidbody GetRigidbody() + { + return m_NetworkRigidbody.Rigidbody; + } + + public bool HadContactWith(ContactEventTransformHelper otherObject) + { + if (otherObject == null) + { + return false; + } + foreach (var contactEvent in m_ContactEvents) + { + if (contactEvent.CollidingBody == otherObject.m_NetworkRigidbody.Rigidbody) + { + return true; + } + } + return false; + } + + protected virtual void CheckToStopMoving() + { + SetHelperState(HadContactWith(Target) ? HelperStates.None : HelperStates.MoveForward); + } + + public void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + if (Target == null) + { + return; + } + + if (collidingBody != null) + { + Log($">>>>>>> contact event with {collidingBody.name}!"); + } + else + { + Log($">>>>>>> contact event with non-rigidbody!"); + } + + m_ContactEvents.Add(new ContactEventInfo() + { + EventId = eventId, + AveragedCollisionNormal = averagedCollisionNormal, + CollidingBody = collidingBody, + ContactPoint = contactPoint, + }); + CheckToStopMoving(); + } + + private void SetInitialPositionClientServer() + { + if (IsServer) + { + if (!NetworkManager.DistributedAuthorityMode && !IsLocalPlayer) + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + else + { + transform.position = SessionOwnerSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; + } + } + else + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + } + + private void SetInitialPositionDistributedAuthority() + { + if (HasAuthority) + { + if (IsSessionOwner) + { + transform.position = SessionOwnerSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; + } + else + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + } + } + + public override void OnNetworkSpawn() + { + m_NetworkRigidbody = GetComponent(); + + m_NetworkRigidbody.Rigidbody.maxLinearVelocity = 15; + m_NetworkRigidbody.Rigidbody.maxAngularVelocity = 10; + + if (NetworkManager.DistributedAuthorityMode) + { + SetInitialPositionDistributedAuthority(); + } + else + { + SetInitialPositionClientServer(); + } + if (IsLocalPlayer) + { + RegisterForContactEvents(true); + } + else + { + m_NetworkRigidbody.Rigidbody.detectCollisions = false; + } + base.OnNetworkSpawn(); + } + + protected virtual void OnRegisterForContactEvents(bool isRegistering) + { + RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); + } + + public void RegisterForContactEvents(bool isRegistering) + { + OnRegisterForContactEvents(isRegistering); + } + + private void FixedUpdate() + { + if (!IsSpawned || !IsOwner || m_HelperState != HelperStates.MoveForward) + { + return; + } + var distance = Vector3.Distance(Target.transform.position, transform.position); + var moveAmount = Mathf.Max(1.2f, distance); + // Head towards our target + var dir = (Target.transform.position - transform.position).normalized; + var deltaMove = dir * moveAmount * Time.fixedDeltaTime; + m_NetworkRigidbody.Rigidbody.MovePosition(m_NetworkRigidbody.Rigidbody.position + deltaMove); + + + Log($" Loc: {transform.position} | Dest: {Target.transform.position} | Dist: {distance} | MoveDelta: {deltaMove}"); + } + + protected void Log(string msg) + { + if (VerboseDebug) + { + Debug.Log($"Client-{OwnerClientId} {msg}"); + } + } + } + + [TestFixture(HostOrServer.Host, ContactEventTypes.Default)] + [TestFixture(HostOrServer.DAHost, ContactEventTypes.Default)] + [TestFixture(HostOrServer.Host, ContactEventTypes.WithInfo)] + [TestFixture(HostOrServer.DAHost, ContactEventTypes.WithInfo)] + internal class RigidbodyContactEventManagerTests : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 1; + + + private GameObject m_RigidbodyContactEventManager; + + public enum ContactEventTypes + { + Default, + WithInfo + } + + private ContactEventTypes m_ContactEventType; + private StringBuilder m_ErrorLogger = new StringBuilder(); + + public RigidbodyContactEventManagerTests(HostOrServer hostOrServer, ContactEventTypes contactEventType) : base(hostOrServer) + { + m_ContactEventType = contactEventType; + } + + protected override void OnCreatePlayerPrefab() + { + ContactEventTransformHelper.SessionOwnerSpawnPoint = GetRandomVector3(-4, -3); + ContactEventTransformHelper.ClientSpawnPoint = GetRandomVector3(3, 4); + if (m_ContactEventType == ContactEventTypes.Default) + { + var helper = m_PlayerPrefab.AddComponent(); + helper.AuthorityMode = NetworkTransform.AuthorityModes.Owner; + } + else + { + var helperWithInfo = m_PlayerPrefab.AddComponent(); + helperWithInfo.AuthorityMode = NetworkTransform.AuthorityModes.Owner; + } + + var rigidbody = m_PlayerPrefab.AddComponent(); + rigidbody.useGravity = false; + rigidbody.isKinematic = true; + rigidbody.mass = 5.0f; + rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous; + var sphereCollider = m_PlayerPrefab.AddComponent(); + sphereCollider.radius = 0.5f; + sphereCollider.providesContacts = true; + + var networkRigidbody = m_PlayerPrefab.AddComponent(); + networkRigidbody.UseRigidBodyForMotion = true; + networkRigidbody.AutoUpdateKinematicState = false; + + m_RigidbodyContactEventManager = new GameObject(); + m_RigidbodyContactEventManager.AddComponent(); + } + + + + private bool PlayersSpawnedInRightLocation() + { + var position = m_ServerNetworkManager.LocalClient.PlayerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); + return false; + } + + position = m_ClientNetworkManagers[0].LocalClient.PlayerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); + return false; + } + var playerObject = (NetworkObject)null; + if (!m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_ClientNetworkManagers[0].LocalClient.PlayerObject.NetworkObjectId)) + { + m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} cannot find a local spawned instance of Client-{m_ClientNetworkManagers[0].LocalClientId}'s player object!"); + return false; + } + playerObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_ClientNetworkManagers[0].LocalClient.PlayerObject.NetworkObjectId]; + position = playerObject.transform.position; + + if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); + return false; + } + + if (!m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects.ContainsKey(m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId)) + { + m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} cannot find a local spawned instance of Client-{m_ServerNetworkManager.LocalClientId}'s player object!"); + return false; + } + playerObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId]; + position = playerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, playerObject.transform.position)) + { + m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); + return false; + } + return true; + } + + + [UnityTest] + public IEnumerator TestContactEvents() + { + ContactEventTransformHelper.VerboseDebug = m_EnableVerboseDebug; + + m_PlayerPrefab.SetActive(false); + m_ErrorLogger.Clear(); + // Validate all instances are spawned in the right location + yield return WaitForConditionOrTimeOut(PlayersSpawnedInRightLocation); + AssertOnTimeout($"Timed out waiting for all player instances to spawn in the corect location:\n {m_ErrorLogger}"); + m_ErrorLogger.Clear(); + + var sessionOwnerPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent() : + m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent(); + var clientPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent() : + m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); + + // Get both players to point towards each other + sessionOwnerPlayer.Target = clientPlayer; + clientPlayer.Target = sessionOwnerPlayer; + + sessionOwnerPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); + clientPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); + + + yield return WaitForConditionOrTimeOut(() => sessionOwnerPlayer.HadContactWith(clientPlayer) || clientPlayer.HadContactWith(sessionOwnerPlayer)); + AssertOnTimeout("Timed out waiting for a player to collide with another player!"); + + clientPlayer.RegisterForContactEvents(false); + sessionOwnerPlayer.RegisterForContactEvents(false); + var otherPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ServerNetworkManager.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent() : + m_ServerNetworkManager.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent(); + otherPlayer.RegisterForContactEvents(false); + otherPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent() : + m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent(); + otherPlayer.RegisterForContactEvents(false); + + Object.Destroy(m_RigidbodyContactEventManager); + m_RigidbodyContactEventManager = null; + } + + protected override IEnumerator OnTearDown() + { + // In case of a test failure + if (m_RigidbodyContactEventManager) + { + Object.Destroy(m_RigidbodyContactEventManager); + m_RigidbodyContactEventManager = null; + } + + return base.OnTearDown(); + } + } } #endif // COM_UNITY_MODULES_PHYSICS diff --git a/Tests/Runtime/PlayerObjectTests.cs b/Tests/Runtime/PlayerObjectTests.cs index 8fba758..e382bcf 100644 --- a/Tests/Runtime/PlayerObjectTests.cs +++ b/Tests/Runtime/PlayerObjectTests.cs @@ -11,7 +11,7 @@ namespace Unity.Netcode.RuntimeTests [TestFixture(HostOrServer.Server)] internal class PlayerObjectTests : NetcodeIntegrationTest { - protected override int NumberOfClients => 1; + protected override int NumberOfClients => 2; protected GameObject m_NewPlayerToSpawn; @@ -52,4 +52,136 @@ public IEnumerator SpawnAndReplaceExistingPlayerObject() Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side player object to change!"); } } + + /// + /// Validate that when auto-player spawning but SpawnWithObservers is disabled, + /// the player instantiated is only spawned on the authority side. + /// + [TestFixture(HostOrServer.DAHost)] + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class PlayerSpawnNoObserversTest : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + public PlayerSpawnNoObserversTest(HostOrServer hostOrServer) : base(hostOrServer) { } + + protected override bool ShouldCheckForSpawnedPlayers() + { + return false; + } + + protected override void OnCreatePlayerPrefab() + { + var playerNetworkObject = m_PlayerPrefab.GetComponent(); + playerNetworkObject.SpawnWithObservers = false; + base.OnCreatePlayerPrefab(); + } + + [UnityTest] + public IEnumerator SpawnWithNoObservers() + { + yield return s_DefaultWaitForTick; + + if (!m_DistributedAuthority) + { + // Make sure clients did not spawn their player object on any of the clients including the owner. + foreach (var client in m_ClientNetworkManagers) + { + foreach (var playerObject in m_ServerNetworkManager.SpawnManager.PlayerObjects) + { + Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObject.NetworkObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{playerObject.NetworkObjectId}!"); + } + } + } + else + { + // For distributed authority, we want to make sure the player object is only spawned on the authority side and all non-authority instances did not spawn it. + var playerObjectId = m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId; + foreach (var client in m_ClientNetworkManagers) + { + Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{m_ServerNetworkManager.LocalClientId}!"); + } + + foreach (var clientPlayer in m_ClientNetworkManagers) + { + playerObjectId = clientPlayer.LocalClient.PlayerObject.NetworkObjectId; + Assert.IsFalse(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{m_ServerNetworkManager.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!"); + foreach (var client in m_ClientNetworkManagers) + { + if (clientPlayer == client) + { + continue; + } + Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!"); + } + } + + } + } + } + + /// + /// This test validates the player position and rotation is correct + /// relative to the prefab's initial settings if no changes are applied. + /// + [TestFixture(HostOrServer.DAHost)] + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class PlayerSpawnPositionTests : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 2; + + public PlayerSpawnPositionTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + private Vector3 m_PlayerPosition; + private Quaternion m_PlayerRotation; + + protected override void OnCreatePlayerPrefab() + { + var playerNetworkObject = m_PlayerPrefab.GetComponent(); + m_PlayerPosition = GetRandomVector3(-10.0f, 10.0f); + m_PlayerRotation = Quaternion.Euler(GetRandomVector3(-180.0f, 180.0f)); + playerNetworkObject.transform.position = m_PlayerPosition; + playerNetworkObject.transform.rotation = m_PlayerRotation; + base.OnCreatePlayerPrefab(); + } + + private void PlayerTransformMatches(NetworkObject player) + { + var position = player.transform.position; + var rotation = player.transform.rotation; + Assert.True(Approximately(m_PlayerPosition, position), $"Client-{player.OwnerClientId} position {position} does not match the prefab position {m_PlayerPosition}!"); + Assert.True(Approximately(m_PlayerRotation, rotation), $"Client-{player.OwnerClientId} rotation {rotation.eulerAngles} does not match the prefab rotation {m_PlayerRotation.eulerAngles}!"); + } + + [UnityTest] + public IEnumerator PlayerSpawnPosition() + { + if (m_ServerNetworkManager.IsHost) + { + PlayerTransformMatches(m_ServerNetworkManager.LocalClient.PlayerObject); + + foreach (var client in m_ClientNetworkManagers) + { + yield return WaitForConditionOrTimeOut(() => client.SpawnManager.SpawnedObjects.ContainsKey(m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId)); + AssertOnTimeout($"Client-{client.LocalClientId} does not contain a player prefab instance for client-{m_ServerNetworkManager.LocalClientId}!"); + PlayerTransformMatches(client.SpawnManager.SpawnedObjects[m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId]); + } + } + + foreach (var client in m_ClientNetworkManagers) + { + yield return WaitForConditionOrTimeOut(() => m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId)); + AssertOnTimeout($"Client-{m_ServerNetworkManager.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!"); + PlayerTransformMatches(m_ServerNetworkManager.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]); + foreach (var subClient in m_ClientNetworkManagers) + { + yield return WaitForConditionOrTimeOut(() => subClient.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId)); + AssertOnTimeout($"Client-{subClient.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!"); + PlayerTransformMatches(subClient.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]); + } + } + } + } } diff --git a/package.json b/package.json index 049964c..ab43e49 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,23 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "2.0.0", + "version": "2.1.1", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", "com.unity.transport": "2.3.0" }, "_upm": { - "changelog": "### Added\n\n- Added tooltips for all of the `NetworkObject` component's properties. (#3052)\n- Added message size validation to named and unnamed message sending functions for better error messages. (#3049)\n- Added \"Check for NetworkObject Component\" property to the Multiplayer->Netcode for GameObjects project settings. When disabled, this will bypass the in-editor `NetworkObject` check on `NetworkBehaviour` components. (#3031)\n- Added `NetworkTransform.SwitchTransformSpaceWhenParented` property that, when enabled, will handle the world to local, local to world, and local to local transform space transitions when interpolation is enabled. (#3013)\n- Added `NetworkTransform.TickSyncChildren` that, when enabled, will tick synchronize nested and/or child `NetworkTransform` components to eliminate any potential visual jittering that could occur if the `NetworkTransform` instances get into a state where their state updates are landing on different network ticks. (#3013)\n- Added `NetworkObject.AllowOwnerToParent` property to provide the ability to allow clients to parent owned objects when running in a client-server network topology. (#3013)\n- Added `NetworkObject.SyncOwnerTransformWhenParented` property to provide a way to disable applying the server's transform information in the parenting message on the client owner instance which can be useful for owner authoritative motion models. (#3013)\n- Added `NetcodeEditorBase` editor helper class to provide easier modification and extension of the SDK's components. (#3013)\n\n### Fixed\n\n- Fixed issue where `NetworkAnimator` would send updates to non-observer clients. (#3057)\n- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3052)\n- Fixed issue where a NetworkObject hidden from a client that is then promoted to be session owner was not being synchronized with newly joining clients.(#3051)\n- Fixed issue where clients could have a wrong time delta on `NetworkVariableBase` which could prevent from sending delta state updates. (#3045)\n- Fixed issue where setting a prefab hash value during connection approval but not having a player prefab assigned could cause an exception when spawning a player. (#3042)\n- Fixed issue where the `NetworkSpawnManager.HandleNetworkObjectShow` could throw an exception if one of the `NetworkObject` components to show was destroyed during the same frame. (#3030)\n- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026)\n- Fixed issue with newly/late joined clients and `NetworkTransform` synchronization of parented `NetworkObject` instances. (#3013)\n- Fixed issue with smooth transitions between transform spaces when interpolation is enabled (requires `NetworkTransform.SwitchTransformSpaceWhenParented` to be enabled). (#3013)\n\n### Changed\n\n- Changed `NetworkTransformEditor` now uses `NetworkTransform` as the base type class to assure it doesn't display a foldout group when using the base `NetworkTransform` component class. (#3052)\n- Changed `NetworkAnimator.Awake` is now a protected virtual method. (#3052)\n- Changed when invoking `NetworkManager.ConnectionManager.DisconnectClient` during a distributed authority session a more appropriate message is logged. (#3052)\n- Changed `NetworkTransformEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkRigidbodyBaseEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkManagerEditor` so it now derives from `NetcodeEditorBase`. (#3013)" + "changelog": "### Added\n\n- Added ability to edit the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` within the inspector view. (#3097)\n- Added `IContactEventHandlerWithInfo` that derives from `IContactEventHandler` that can be updated per frame to provide `ContactEventHandlerInfo` information to the `RigidbodyContactEventManager` when processing collisions. (#3094)\n - `ContactEventHandlerInfo.ProvideNonRigidBodyContactEvents`: When set to true, non-`Rigidbody` collisions with the registered `Rigidbody` will generate contact event notifications. (#3094)\n - `ContactEventHandlerInfo.HasContactEventPriority`: When set to true, the `Rigidbody` will be prioritized as the instance that generates the event if the `Rigidbody` colliding does not have priority. (#3094)\n- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088)\n- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088)\n\n### Fixed\n\n- Fixed issue where `NetworkPrefabProcessor` would not mark the prefab list as dirty and prevent saving the `DefaultNetworkPrefabs` asset when only imports or only deletes were detected.(#3103)\n- Fixed an issue where nested `NetworkTransform` components in owner authoritative mode cleared their initial settings on the server, causing improper synchronization. (#3099)\n- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096)\n- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092)\n- Fixed an issue where newly synchronizing clients would always receive current `NetworkVariable` values, potentially causing issues with collections if there were pending updates. Now, pending state updates serialize previous values to avoid duplicates on new clients. (#3081)\n- Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081)\n- Fixed an issue where transferring ownership of `NetworkVariable` collections didn't update the new owner’s previous value, causing the last added value to be detected as a change during additions or removals. (#3081)\n- Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081)\n- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078)\n- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077)\n- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075)\n\n### Changed\n\n- Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097)\n- Updated `NetworkVariableDeltaMessage` so the server now forwards delta state updates from clients immediately, instead of waiting until the end of the frame or the next network tick. (#3081)" }, "upmCi": { - "footprint": "f1ef7566b7a89b1ee9c34cc13400735ae63964d4" + "footprint": "8331c76150e539e36659d8b7be3ba0fb6d21027a" }, - "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.0/manual/index.html", + "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.1/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "8a7ae9f91a53bdcabe5e7df783dd1884c07bcd6f" + "revision": "264b30d176dd71fcedd022a8d6f4d59a2e3922bc" }, "samples": [ {