diff --git a/.signature b/.signature new file mode 100644 index 0000000..859cb36 --- /dev/null +++ b/.signature @@ -0,0 +1 @@ +{"timestamp":1734038821,"signature":"IRr6jlCcty11r8we1Ez4y7wx0e3FCIoANe7HGxeqQsSe+qAtXm7XbKkHoTpjhKgBTArN/+znpDoPZAGHy4oJnk7IgGDMorB5fPrG6hn4XRsaJgm1nSzV4KCmmMbV8EKO4NWqnlNIhtgO3Wvl9r+FvJlXOBZvvqExI0TGmBqM24rQT8KUns5JlVug1HcMt4CM5Gl7mbUOosxK4BGBhSyPi4xNNAqkWQnPlpK0cbF59zHNCphnctN5ANidsKjhUkLnCNAusukN5c99OMEsUakmJiCGcnzX0J0ynnR6XRGR6jPJbiK6h2kh+apQ6OuXP86aP0Aip2oZUBPRUw8nFf6YdlO/8MqASbLVryFqrUqryjfFFMzok+fZHPFyBmAuaV8X+heAXp7eKWWiRXuT7NeKD8gG6wo1QckT42i5tHJZPC1ovUEIAKqf747UmElQZWaLYIHHHmjCHV2lAAz8hZj1zo379u+epBeErG9Z71HqGrUHzLlEsGgCUOq9Vb+KXKyz","publicKey":"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQm9qQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FZOEFNSUlCaWdLQ0FZRUFzdUhXYUhsZ0I1cVF4ZEJjTlJKSAordHR4SmoxcVY1NTdvMlZaRE1XaXhYRVBkRTBEMVFkT1JIRXNSS1RscmplUXlERU83ZlNQS0ZwZ1A3MU5TTnJCCkFHM2NFSU45aHNQVDhOVmllZmdWem5QTkVMenFkVmdEbFhpb2VpUnV6OERKWFgvblpmU1JWKytwbk9ySTRibG4KS0twelJlNW14OTc1SjhxZ1FvRktKT0NNRlpHdkJMR2MxSzZZaEIzOHJFODZCZzgzbUovWjBEYkVmQjBxZm13cgo2ZDVFUXFsd0E5Y3JZT1YyV1VpWXprSnBLNmJZNzRZNmM1TmpBcEFKeGNiaTFOaDlRVEhUcU44N0ZtMDF0R1ZwCjVNd1pXSWZuYVRUemEvTGZLelR5U0pka0tldEZMVGdkYXpMYlpzUEE2aHBSK0FJRTJhc0tLTi84UUk1N3UzU2cKL2xyMnZKS1IvU2l5eEN1Q20vQWJkYnJMbXk0WjlSdm1jMGdpclA4T0lLQWxBRWZ2TzV5Z2hSKy8vd1RpTFlzUQp1SllDM0V2UE16ZGdKUzdGR2FscnFLZzlPTCsxVzROY05yNWdveVdSUUJ0cktKaWlTZEJVWmVxb0RvSUY5NHpCCndGbzJJT1JFdXFqcU51M3diMWZIM3p1dGdtalFra3IxVjJhd3hmcExLWlROQWdNQkFBRT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dfea0a2..8aa5de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,38 @@ 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). +## [1.12.0] - 2024-11-19 + +### Added + +- Added `UnityTransport.GetEndpoint` method to provide a way to obtain `NetworkEndpoint` information of a connection via client identifier. (#3131) +- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3089) +- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3089) +- Added message size validation to named and unnamed message sending functions for better error messages. (#3043) +- 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. (#3034) + +### Fixed + +- Fixed issue where `NetworkList` properties on in-scene placed `NetworkObject`s could cause small memory leaks when entering playmode. (#3148) +- Fixed issue where a newly synchronizing client would be synchronized with the current `NetworkVariable` values always which could cause issues with collections if there were any pending state updates. Now, when initially synchronizing a client, if a `NetworkVariable` has a pending state update it will serialize the previously known value(s) to the synchronizing client so when the pending updates are sent they aren't duplicate values on the newly connected client side. (#3126) +- 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. (#3126) +- Fixed issue with `NetworkVariable` collections where transferring ownership to another client would not update the new owner's previous value to the most current value which could cause the last/previous added value to be detected as a change when adding or removing an entry (as long as the entry removed was not the last/previously added value). (#3126) +- 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. (#3126) +- Fixed issue where `NetworkAnimator` would statically allocate write buffer space for `Animator` parameters that could cause a write error if the number of parameters exceeded the space allocated. (#3124) +- 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. (#3084) +- Fixed issue where `NetworkAnimator` would send updates to non-observer clients. (#3058) +- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3055) +- 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. (#3046) +- Fixed issue where collections v2.2.x was not supported when using UTP v2.2.x within Unity v2022.3. (#3033) +- Fixed issue where the `NetworkSpawnManager.HandleNetworkObjectShow` could throw an exception if one of the `NetworkObject` components to show was destroyed during the same frame. (#3029) +- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3027) + +### Changed + +- Changed `NetworkVariableDeltaMessage` so the server now forwards delta state updates (owner write permission based from a client) to other clients immediately as opposed to keeping a `NetworkVariable` or `NetworkList` dirty and processing them at the end of the frame or potentially on the next network tick. (#3126) +- The Debug Simulator section of the Unity Transport component will now be hidden if Unity Transport 2.0 or later is installed. It was already non-functional in that situation and users should instead use the more featureful [Network Simulator](https://docs-multiplayer.unity3d.com/tools/current/tools-network-simulator/) tool from the Multiplayer Tools package. (#3120) + + ## [1.11.0] - 2024-08-20 ### Added @@ -236,7 +268,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed issue where invalid endpoint addresses were not being detected and returning false from NGO UnityTransport. (#2496) - Fixed some errors that could occur if a connection is lost and the loss is detected when attempting to write to the socket. (#2495) -## Changed +### Changed - Adding network prefabs before NetworkManager initialization is now supported. (#2565) - Connecting clients being synchronized now switch to the server's active scene before spawning and synchronizing NetworkObjects. (#2532) diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 4eee6a0..f588f81 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -481,10 +481,6 @@ protected virtual bool OnIsServerAuthoritative() return true; } - // Animators only support up to 32 parameters - // TODO: Look into making this a range limited property - private const int k_MaxAnimationParams = 32; - private int[] m_TransitionHash; private int[] m_AnimationHash; private float[] m_LayerWeights; @@ -508,7 +504,7 @@ private unsafe struct AnimatorParamCache } // 128 bytes per Animator - private FastBufferWriter m_ParameterWriter = new FastBufferWriter(k_MaxAnimationParams * sizeof(float), Allocator.Persistent); + private FastBufferWriter m_ParameterWriter; private NativeArray m_CachedAnimatorParameters; @@ -560,6 +556,14 @@ public override void OnDestroy() private void Awake() { + if (!m_Animator) + { +#if !UNITY_EDITOR + Debug.LogError($"{nameof(NetworkAnimator)} {name} does not have an {nameof(UnityEngine.Animator)} assigned to it. The {nameof(NetworkAnimator)} will not initialize properly."); +#endif + return; + } + int layers = m_Animator.layerCount; // Initializing the below arrays for everyone handles an issue // when running in owner authoritative mode and the owner changes. @@ -589,6 +593,9 @@ private void Awake() } } + // The total initialization size calculated for the m_ParameterWriter write buffer. + var totalParameterSize = sizeof(uint); + // Build our reference parameter values to detect when they change var parameters = m_Animator.parameters; m_CachedAnimatorParameters = new NativeArray(parameters.Length, Allocator.Persistent); @@ -629,7 +636,37 @@ private void Awake() } m_CachedAnimatorParameters[i] = cacheParam; + + // Calculate parameter sizes (index + type size) + switch (parameter.type) + { + case AnimatorControllerParameterType.Int: + { + totalParameterSize += sizeof(int) * 2; + break; + } + case AnimatorControllerParameterType.Bool: + case AnimatorControllerParameterType.Trigger: + { + // Bool is serialized to 1 byte + totalParameterSize += sizeof(int) + 1; + break; + } + case AnimatorControllerParameterType.Float: + { + totalParameterSize += sizeof(int) + sizeof(float); + break; + } + } + } + + if (m_ParameterWriter.IsInitialized) + { + m_ParameterWriter.Dispose(); } + + // Create our parameter write buffer for serialization + m_ParameterWriter = new FastBufferWriter(totalParameterSize, Allocator.Persistent); } /// @@ -924,8 +961,14 @@ internal void CheckForAnimatorChanges() { // Just notify all remote clients and not the local server m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(NetworkManager.LocalClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams); } @@ -1223,9 +1266,14 @@ private unsafe void SendParametersUpdateServerRpc(ParametersUpdateMessage parame if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); - m_ClientSendList.Remove(NetworkManager.ServerClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == serverRpcParams.Receive.SenderClientId || clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; m_NetworkAnimatorStateChangeHandler.SendParameterUpdate(parametersUpdate, m_ClientRpcParams); } @@ -1271,9 +1319,14 @@ private unsafe void SendAnimStateServerRpc(AnimationMessage animationMessage, Se if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); - m_ClientSendList.Remove(NetworkManager.ServerClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == serverRpcParams.Receive.SenderClientId || clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage, m_ClientRpcParams); } @@ -1322,8 +1375,14 @@ internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerM InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(NetworkManager.ServerClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } if (IsServerAuthoritative()) { diff --git a/Editor/Configuration/NetcodeForGameObjectsSettings.cs b/Editor/Configuration/NetcodeForGameObjectsSettings.cs index 4fc4b0c..70dfc02 100644 --- a/Editor/Configuration/NetcodeForGameObjectsSettings.cs +++ b/Editor/Configuration/NetcodeForGameObjectsSettings.cs @@ -5,6 +5,7 @@ namespace Unity.Netcode.Editor.Configuration internal class NetcodeForGameObjectsEditorSettings { internal const string AutoAddNetworkObjectIfNoneExists = "AutoAdd-NetworkObject-When-None-Exist"; + internal const string CheckForNetworkObject = "NetworkBehaviour-Check-For-NetworkObject"; internal const string InstallMultiplayerToolsTipDismissedPlayerPrefKey = "Netcode_Tip_InstallMPTools_Dismissed"; internal static int GetNetcodeInstallMultiplayerToolTips() @@ -28,7 +29,7 @@ internal static bool GetAutoAddNetworkObjectSetting() { return EditorPrefs.GetBool(AutoAddNetworkObjectIfNoneExists); } - + // Default for this is false return false; } @@ -36,5 +37,20 @@ internal static void SetAutoAddNetworkObjectSetting(bool autoAddSetting) { EditorPrefs.SetBool(AutoAddNetworkObjectIfNoneExists, autoAddSetting); } + + internal static bool GetCheckForNetworkObjectSetting() + { + if (EditorPrefs.HasKey(CheckForNetworkObject)) + { + return EditorPrefs.GetBool(CheckForNetworkObject); + } + // Default for this is true + return true; + } + + internal static void SetCheckForNetworkObjectSetting(bool checkForNetworkObject) + { + EditorPrefs.SetBool(CheckForNetworkObject, checkForNetworkObject); + } } } diff --git a/Editor/Configuration/NetcodeSettingsProvider.cs b/Editor/Configuration/NetcodeSettingsProvider.cs index ce8023b..0f78c43 100644 --- a/Editor/Configuration/NetcodeSettingsProvider.cs +++ b/Editor/Configuration/NetcodeSettingsProvider.cs @@ -81,6 +81,7 @@ private static void OnDeactivate() internal static NetcodeSettingsLabel NetworkObjectsSectionLabel; internal static NetcodeSettingsToggle AutoAddNetworkObjectToggle; + internal static NetcodeSettingsToggle CheckForNetworkObjectToggle; internal static NetcodeSettingsLabel MultiplayerToolsLabel; internal static NetcodeSettingsToggle MultiplayerToolTipStatusToggle; @@ -103,6 +104,11 @@ private static void CheckForInitialize() AutoAddNetworkObjectToggle = new NetcodeSettingsToggle("Auto-Add NetworkObject Component", "When enabled, NetworkObject components are automatically added to GameObjects when NetworkBehaviour components are added first.", 20); } + if (CheckForNetworkObjectToggle == null) + { + CheckForNetworkObjectToggle = new NetcodeSettingsToggle("Check for NetworkObject Component", "When disabled, the automatic check on NetworkBehaviours for an associated NetworkObject component will not be performed and Auto-Add NetworkObject Component will be disabled.", 20); + } + if (MultiplayerToolsLabel == null) { MultiplayerToolsLabel = new NetcodeSettingsLabel("Multiplayer Tools", 20); @@ -120,6 +126,7 @@ private static void OnGuiHandler(string obj) CheckForInitialize(); var autoAddNetworkObjectSetting = NetcodeForGameObjectsEditorSettings.GetAutoAddNetworkObjectSetting(); + var checkForNetworkObjectSetting = NetcodeForGameObjectsEditorSettings.GetCheckForNetworkObjectSetting(); var multiplayerToolsTipStatus = NetcodeForGameObjectsEditorSettings.GetNetcodeInstallMultiplayerToolTips() == 0; var settings = NetcodeForGameObjectsProjectSettings.instance; var generateDefaultPrefabs = settings.GenerateDefaultNetworkPrefabs; @@ -134,7 +141,12 @@ private static void OnGuiHandler(string obj) { GUILayout.BeginVertical("Box"); NetworkObjectsSectionLabel.DrawLabel(); - autoAddNetworkObjectSetting = AutoAddNetworkObjectToggle.DrawToggle(autoAddNetworkObjectSetting); + autoAddNetworkObjectSetting = AutoAddNetworkObjectToggle.DrawToggle(autoAddNetworkObjectSetting, checkForNetworkObjectSetting); + checkForNetworkObjectSetting = CheckForNetworkObjectToggle.DrawToggle(checkForNetworkObjectSetting); + if (autoAddNetworkObjectSetting && !checkForNetworkObjectSetting) + { + autoAddNetworkObjectSetting = false; + } GUILayout.EndVertical(); GUILayout.BeginVertical("Box"); @@ -184,6 +196,7 @@ private static void OnGuiHandler(string obj) if (EditorGUI.EndChangeCheck()) { NetcodeForGameObjectsEditorSettings.SetAutoAddNetworkObjectSetting(autoAddNetworkObjectSetting); + NetcodeForGameObjectsEditorSettings.SetCheckForNetworkObjectSetting(checkForNetworkObjectSetting); NetcodeForGameObjectsEditorSettings.SetNetcodeInstallMultiplayerToolTips(multiplayerToolsTipStatus ? 0 : 1); settings.GenerateDefaultNetworkPrefabs = generateDefaultPrefabs; settings.TempNetworkPrefabsPath = networkPrefabsPath; @@ -213,10 +226,13 @@ internal class NetcodeSettingsToggle : NetcodeGUISettings { private GUIContent m_ToggleContent; - public bool DrawToggle(bool currentSetting) + public bool DrawToggle(bool currentSetting, bool enabled = true) { EditorGUIUtility.labelWidth = m_LabelSize; - return EditorGUILayout.Toggle(m_ToggleContent, currentSetting, m_LayoutWidth); + GUI.enabled = enabled; + var returnValue = EditorGUILayout.Toggle(m_ToggleContent, currentSetting, m_LayoutWidth); + GUI.enabled = true; + return returnValue; } public NetcodeSettingsToggle(string labelText, string toolTip, float layoutOffset) 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/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs index 7d57afb..c98870c 100644 --- a/Editor/NetworkBehaviourEditor.cs +++ b/Editor/NetworkBehaviourEditor.cs @@ -352,6 +352,12 @@ public static void CheckForNetworkObject(GameObject gameObject, bool networkObje return; } + // If this automatic check is disabled, then do not perform this check. + if (!NetcodeForGameObjectsEditorSettings.GetCheckForNetworkObjectSetting()) + { + return; + } + // Now get the root parent transform to the current GameObject (or itself) var rootTransform = GetRootParentTransform(gameObject.transform); if (!rootTransform.TryGetComponent(out var networkManager)) diff --git a/Editor/NetworkManagerHelper.cs b/Editor/NetworkManagerHelper.cs index 19643d4..3138369 100644 --- a/Editor/NetworkManagerHelper.cs +++ b/Editor/NetworkManagerHelper.cs @@ -61,6 +61,12 @@ private static void EditorApplication_playModeStateChanged(PlayModeStateChange p { s_LastKnownNetworkManagerParents.Clear(); ScenesInBuildActiveSceneCheck(); + EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; + break; + } + case PlayModeStateChange.EnteredEditMode: + { + EditorApplication.hierarchyChanged += EditorApplication_hierarchyChanged; break; } } @@ -110,6 +116,12 @@ private static void ScenesInBuildActiveSceneCheck() /// private static void EditorApplication_hierarchyChanged() { + if (Application.isPlaying) + { + EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; + return; + } + var allNetworkManagers = Resources.FindObjectsOfTypeAll(); foreach (var networkManager in allNetworkManagers) { diff --git a/LICENSE.md b/LICENSE.md index a5eb171..566cc12 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Unity Technologies +com.unity.netcode.gameobjects copyright © 2024 Unity Technologies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs index ce7b8f4..8504ad0 100644 --- a/Runtime/Connection/NetworkConnectionManager.cs +++ b/Runtime/Connection/NetworkConnectionManager.cs @@ -10,15 +10,54 @@ namespace Unity.Netcode { - + /// + /// The connection event type set within to signify the type of connection event notification received. + /// + /// + /// is returned as a parameter of the event notification. + /// and event types occur on the client-side of the newly connected client and on the server-side.
+ /// and event types occur on connected clients to notify that a new client (peer) has joined/connected. + ///
public enum ConnectionEvent { + /// + /// This event is set on the client-side of the newly connected client and on the server-side.
+ ///
+ /// + /// On the newly connected client side, the will be the .
+ /// On the server side, the will be the ID of the client that just connected. + ///
ClientConnected, + /// + /// This event is set on clients that are already connected to the session. + /// + /// + /// The will be the ID of the client that just connected. + /// PeerConnected, + /// + /// This event is set on the client-side of the client that disconnected client and on the server-side. + /// + /// + /// On the disconnected client side, the will be the .
+ /// On the server side, this will be the ID of the client that disconnected. + ///
ClientDisconnected, + /// + /// This event is set on clients that are already connected to the session. + /// + /// + /// The will be the ID of the client that just disconnected. + /// PeerDisconnected } + /// + /// Returned as a parameter of the event notification. + /// + /// + /// See for more details on the types of connection events received. + /// public struct ConnectionEventData { public ConnectionEvent EventType; @@ -736,41 +775,20 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne var client = AddClient(ownerClientId); - if (response.CreatePlayerObject) + if (response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) { - var prefabNetworkObject = NetworkManager.NetworkConfig.PlayerPrefab.GetComponent(); - var playerPrefabHash = response.PlayerPrefabHash ?? prefabNetworkObject.GlobalObjectIdHash; - - // Generate a SceneObject for the player object to spawn - // Note: This is only to create the local NetworkObject, many of the serialized properties of the player prefab will be set when instantiated. - var sceneObject = new NetworkObject.SceneObject - { - OwnerClientId = ownerClientId, - IsPlayerObject = true, - IsSceneObject = false, - HasTransform = prefabNetworkObject.SynchronizeTransform, - Hash = playerPrefabHash, - TargetClientId = ownerClientId, - Transform = new NetworkObject.SceneObject.TransformData - { - Position = response.Position.GetValueOrDefault(), - Rotation = response.Rotation.GetValueOrDefault() - } - }; - - // Create the player NetworkObject locally - var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); - + 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( - networkObject, + playerObject, NetworkManager.SpawnManager.GetNetworkObjectId(), sceneObject: false, playerObject: true, ownerClientId, destroyWithScene: false); - client.AssignPlayerObject(ref networkObject); + client.AssignPlayerObject(ref playerObject); } // Server doesn't send itself the connection approved message diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 5e4ce50..25f1349 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -765,6 +765,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(); } @@ -946,20 +953,20 @@ internal void PreVariableUpdate() PreNetworkVariableWrite(); } - internal void VariableUpdate(ulong targetClientId) - { - NetworkVariableUpdate(targetClientId, NetworkBehaviourId); - } - internal readonly List NetworkVariableIndexesToReset = new List(); internal readonly HashSet NetworkVariableIndexesToResetSet = new HashSet(); - private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex) + internal void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex, bool forceSend = false) { - if (!CouldHaveDirtyNetworkVariables()) + if (!forceSend && !CouldHaveDirtyNetworkVariables()) { return; } + // Getting these ahead of time actually improves performance + var networkManager = NetworkManager; + var networkObject = NetworkObject; + var messageManager = networkManager.MessageManager; + var connectionManager = networkManager.ConnectionManager; for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++) { @@ -982,10 +989,14 @@ private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex) var message = new NetworkVariableDeltaMessage { NetworkObjectId = NetworkObjectId, - NetworkBehaviourIndex = NetworkObject.GetNetworkBehaviourOrderIndex(this), + NetworkBehaviourIndex = networkObject.GetNetworkBehaviourOrderIndex(this), 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, @@ -994,7 +1005,7 @@ private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex) // so we don't have to do this serialization work if we're not going to use the result. if (IsServer && targetClientId == NetworkManager.ServerClientId) { - var tmpWriter = new FastBufferWriter(NetworkManager.MessageManager.NonFragmentedMessageMaxSize, Allocator.Temp, NetworkManager.MessageManager.FragmentedMessageMaxSize); + var tmpWriter = new FastBufferWriter(messageManager.NonFragmentedMessageMaxSize, Allocator.Temp, messageManager.FragmentedMessageMaxSize); using (tmpWriter) { message.Serialize(tmpWriter, message.Version); @@ -1002,7 +1013,7 @@ private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex) } else { - NetworkManager.ConnectionManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], targetClientId); + connectionManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], targetClientId); } } } @@ -1029,6 +1040,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++) @@ -1037,6 +1068,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) @@ -1067,7 +1109,11 @@ 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); writer.WriteValueSafe((ushort)size); @@ -1075,7 +1121,11 @@ internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClie } else { - 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 // Only if EnsureNetworkVariableLengthSafety, otherwise just skip diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index 67487ce..1453871 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(); @@ -54,7 +59,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].VariableUpdate(client.ClientId); + dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, k, forceSend); } } } @@ -73,7 +78,7 @@ internal void NetworkBehaviourUpdate() } for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) { - sobj.ChildNetworkBehaviours[k].VariableUpdate(NetworkManager.ServerClientId); + sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, k, forceSend); } } } @@ -86,19 +91,28 @@ 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); } + // Set to true for NetworkVariable to ignore duplication of the + // "internal original value" for collections support. + 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; } m_DirtyNetworkObjects.Clear(); } diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 66ac219..f258189 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -16,6 +16,16 @@ 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; + // TODO: Deprecate... // The following internal values are not used, but because ILPP makes them public in the assembly, they cannot // be removed thanks to our semver validation. @@ -715,6 +725,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() @@ -1274,6 +1286,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 1c95684..3f62c09 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -74,6 +74,7 @@ public uint PrefabIdHash internal void RefreshAllPrefabInstances() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); + NetworkObjectRefreshTool.PrefabNetworkObject = this; if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != k_ImportedAssetObjectType) { EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok"); @@ -81,11 +82,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 @@ -98,14 +94,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)) @@ -197,6 +193,7 @@ private void CheckForInScenePlaced() if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) { InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; + EditorUtility.SetDirty(this); } IsSceneObject = true; } @@ -275,6 +272,8 @@ private GlobalObjectId GetGlobalId() /// public ulong OwnerClientId { get; internal set; } + internal ulong PreviousOwnerId; + /// /// If true, the object will always be replicated as root on clients and the parent will be ignored. /// @@ -1487,6 +1486,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 @@ -1728,11 +1735,11 @@ public void Deserialize(FastBufferReader reader) } } - internal void PostNetworkVariableWrite() + internal void PostNetworkVariableWrite(bool forceSend) { for (int k = 0; k < ChildNetworkBehaviours.Count; k++) { - ChildNetworkBehaviours[k].PostNetworkVariableWrite(); + ChildNetworkBehaviours[k].PostNetworkVariableWrite(forceSend); } } 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/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs index 620e5bb..98c344f 100644 --- a/Runtime/Messaging/CustomMessageManager.cs +++ b/Runtime/Messaging/CustomMessageManager.cs @@ -73,6 +73,8 @@ public void SendUnnamedMessage(IReadOnlyList clientIds, FastBufferWriter throw new ArgumentNullException(nameof(clientIds), "You must pass in a valid clientId List"); } + ValidateMessageSize(messageBuffer, networkDelivery, isNamed: false); + if (m_NetworkManager.IsHost) { for (var i = 0; i < clientIds.Count; ++i) @@ -108,6 +110,8 @@ public void SendUnnamedMessage(IReadOnlyList clientIds, FastBufferWriter /// The delivery type (QoS) to send data with public void SendUnnamedMessage(ulong clientId, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) { + ValidateMessageSize(messageBuffer, networkDelivery, isNamed: false); + if (m_NetworkManager.IsHost) { if (clientId == m_NetworkManager.LocalClientId) @@ -263,6 +267,8 @@ public void SendNamedMessageToAll(string messageName, FastBufferWriter messageSt /// The delivery type (QoS) to send data with public void SendNamedMessage(string messageName, ulong clientId, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) { + ValidateMessageSize(messageStream, networkDelivery, isNamed: true); + ulong hash = 0; switch (m_NetworkManager.NetworkConfig.RpcHashSize) { @@ -321,6 +327,8 @@ public void SendNamedMessage(string messageName, IReadOnlyList clientIds, throw new ArgumentNullException(nameof(clientIds), "You must pass in a valid clientId List"); } + ValidateMessageSize(messageStream, networkDelivery, isNamed: true); + ulong hash = 0; switch (m_NetworkManager.NetworkConfig.RpcHashSize) { @@ -359,5 +367,32 @@ public void SendNamedMessage(string messageName, IReadOnlyList clientIds, m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientIds, messageName, size); } } + + /// + /// Validate the size of the message. If it's a non-fragmented delivery type the message must fit within the + /// max allowed size with headers also subtracted. Named messages also include the hash + /// of the name string. Only validates in editor and development builds. + /// + /// The named message payload + /// Delivery method + /// Is the message named (or unnamed) + /// Exception thrown in case validation fails + private unsafe void ValidateMessageSize(FastBufferWriter messageStream, NetworkDelivery networkDelivery, bool isNamed) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + var maxNonFragmentedSize = m_NetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(NetworkBatchHeader); + if (isNamed) + { + maxNonFragmentedSize -= sizeof(ulong); // MessageName hash + } + if (networkDelivery != NetworkDelivery.ReliableFragmentedSequenced + && messageStream.Length > maxNonFragmentedSize) + { + throw new OverflowException($"Given message size ({messageStream.Length} bytes) is greater than " + + $"the maximum allowed for the selected delivery method ({maxNonFragmentedSize} bytes). Try using " + + $"ReliableFragmentedSequenced delivery method instead."); + } +#endif + } } } diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs index b0d1ca7..43949ba 100644 --- a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -45,12 +45,6 @@ public void Handle(ref NetworkContext context) networkObject.InvokeBehaviourOnLostOwnership(); } - // We are new owner. - if (OwnerClientId == networkManager.LocalClientId) - { - networkObject.InvokeBehaviourOnGainedOwnership(); - } - // For all other clients that are neither the former or current owner, update the behaviours' properties if (OwnerClientId != networkManager.LocalClientId && originalOwner != networkManager.LocalClientId) { @@ -60,6 +54,21 @@ public void Handle(ref NetworkContext context) } } + // We are new owner. + if (OwnerClientId == networkManager.LocalClientId) + { + networkObject.InvokeBehaviourOnGainedOwnership(); + } + + if (originalOwner == 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); + } + networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId); networkManager.NetworkMetrics.TrackOwnershipChangeReceived(context.SenderId, networkObject, context.MessageSize); diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs index 9652869..fed46a1 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,8 +35,42 @@ 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 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 + { + networkVariable.WriteDelta(writer); + } + } + public void Serialize(FastBufferWriter writer, int targetVersion) { if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(NetworkObjectId) + FastBufferWriter.GetWriteSize(NetworkBehaviourIndex))) @@ -32,16 +80,56 @@ 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; BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, NetworkBehaviourIndex); + // 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) + { + 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 (ensureNetworkVariableLengthSafety) + { + if (!shouldWrite) + { + BytePacker.WriteValueBitPacked(writer, (ushort)0); + } + } + else + { + writer.WriteValueSafe(shouldWrite); + } + + if (shouldWrite) + { + WriteNetworkVariable(ref writer, ref networkVariable, ensureNetworkVariableLengthSafety, nonFragmentedMessageMaxSize, fragmentedMessageMaxSize); + networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(TargetClientId, obj, networkVariable.Name, typeName, writer.Length - startingSize); + } + } + return; + } + } + for (int i = 0; i < NetworkBehaviour.NetworkVariableFields.Count; i++) { if (!DeliveryMappedNetworkVariableIndex.Contains(i)) { - // This var does not belong to the currently iterating delivery group. - if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (ensureNetworkVariableLengthSafety) { BytePacker.WriteValueBitPacked(writer, (ushort)0); } @@ -55,7 +143,6 @@ public void Serialize(FastBufferWriter writer, int targetVersion) var startingSize = writer.Length; var networkVariable = NetworkBehaviour.NetworkVariableFields[i]; - var shouldWrite = networkVariable.IsDirty() && networkVariable.CanClientRead(TargetClientId) && (networkManager.IsServer || networkVariable.CanClientWrite(networkManager.LocalClientId)) && @@ -79,7 +166,7 @@ public void Serialize(FastBufferWriter writer, int targetVersion) shouldWrite = false; } - if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (ensureNetworkVariableLengthSafety) { if (!shouldWrite) { @@ -93,38 +180,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 - { - networkVariable.WriteDelta(writer); - } - networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( - TargetClientId, - obj, - networkVariable.Name, - NetworkBehaviour.__getTypeName(), - writer.Length - startingSize); + WriteNetworkVariable(ref writer, ref networkVariable, 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; @@ -136,7 +207,11 @@ public void Handle(ref NetworkContext context) if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject)) { + 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) { @@ -147,13 +222,31 @@ public void Handle(ref NetworkContext context) } else { + // (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.NetworkConfig.EnsureNetworkVariableLengthSafety) + var networkVariable = networkBehaviour.NetworkVariableFields[i]; + + if (ensureNetworkVariableLengthSafety) { ByteUnpacker.ReadValueBitPacked(m_ReceivedNetworkVariableData, out varSize); - if (varSize == 0) { continue; @@ -168,8 +261,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 @@ -197,12 +288,57 @@ 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; - networkVariable.ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer); + if (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, @@ -211,7 +347,7 @@ public void Handle(ref NetworkContext context) networkBehaviour.__getTypeName(), context.MessageSize); - if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (ensureNetworkVariableLengthSafety) { if (m_ReceivedNetworkVariableData.Position > (readStartPos + varSize)) { @@ -233,6 +369,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/Messaging/Messages/ProxyMessage.cs b/Runtime/Messaging/Messages/ProxyMessage.cs index f47237c..7a77934 100644 --- a/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/Runtime/Messaging/Messages/ProxyMessage.cs @@ -1,4 +1,3 @@ -using System; using Unity.Collections; namespace Unity.Netcode @@ -35,7 +34,13 @@ public unsafe void Handle(ref NetworkContext context) var networkManager = (NetworkManager)context.SystemOwner; if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(WrappedMessage.Metadata.NetworkObjectId, out var networkObject)) { - throw new InvalidOperationException($"An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + // If the NetworkObject no longer exists then just log a warning when developer mode logging is enabled and exit. + // This can happen if NetworkObject is despawned and a client sends an RPC before receiving the despawn message. + if (networkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"[{WrappedMessage.Metadata.NetworkObjectId}, {WrappedMessage.Metadata.NetworkBehaviourId}, {WrappedMessage.Metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + } + return; } var observers = networkObject.Observers; diff --git a/Runtime/Messaging/Messages/RpcMessages.cs b/Runtime/Messaging/Messages/RpcMessages.cs index 6ef4c54..29a93c7 100644 --- a/Runtime/Messaging/Messages/RpcMessages.cs +++ b/Runtime/Messaging/Messages/RpcMessages.cs @@ -61,7 +61,13 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, var networkManager = (NetworkManager)context.SystemOwner; if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(metadata.NetworkObjectId, out var networkObject)) { - throw new InvalidOperationException($"An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + // If the NetworkObject no longer exists then just log a warning when developer mode logging is enabled and exit. + // This can happen if NetworkObject is despawned and a client sends an RPC before receiving the despawn message. + if (networkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + } + return; } var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); diff --git a/Runtime/Messaging/NetworkMessageManager.cs b/Runtime/Messaging/NetworkMessageManager.cs index 24699eb..7a89e30 100644 --- a/Runtime/Messaging/NetworkMessageManager.cs +++ b/Runtime/Messaging/NetworkMessageManager.cs @@ -730,7 +730,11 @@ internal unsafe int SendPreSerializedMessage(in FastBufferWriter t } ref var writeQueueItem = ref sendQueueItem.ElementAt(sendQueueItem.Length - 1); - writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + headerSerializer.Length); + if (!writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + headerSerializer.Length)) + { + Debug.LogError($"Not enough space to write message, size={tmpSerializer.Length + headerSerializer.Length} space used={writeQueueItem.Writer.Position} total size={writeQueueItem.Writer.Capacity}"); + continue; + } writeQueueItem.Writer.WriteBytes(headerSerializer.GetUnsafePtr(), headerSerializer.Length); writeQueueItem.Writer.WriteBytes(tmpSerializer.GetUnsafePtr(), tmpSerializer.Length); diff --git a/Runtime/Messaging/RpcTargets/RpcTarget.cs b/Runtime/Messaging/RpcTargets/RpcTarget.cs index 8d99c94..7d55209 100644 --- a/Runtime/Messaging/RpcTargets/RpcTarget.cs +++ b/Runtime/Messaging/RpcTargets/RpcTarget.cs @@ -67,9 +67,28 @@ public enum SendTo SpecifiedInParams } + /// + /// This parameter configures a performance optimization. This optimization is not valid in all situations.
+ /// Because BaseRpcTarget is a managed type, allocating a new one is expensive, as it puts pressure on the garbage collector. + ///
+ /// + /// When using a allocation type for the RPC target(s):
+ /// You typically don't need to worry about persisting the generated. + /// When using a allocation type for the RPC target(s):
+ /// You will want to use , which returns , during initialization (i.e. ) and it to a property.
+ /// Then, When invoking the RPC, you would use your which is a persisted allocation of a given set of client identifiers. + /// !! Important !!
+ /// You will want to invoke of any persisted properties created via when despawning or destroying the associated component's . Not doing so will result in small memory leaks. + ///
public enum RpcTargetUse { + /// + /// Creates a temporary used for the frame an decorated method is invoked. + /// Temp, + /// + /// Creates a persisted that does not change and will persist until is called. + /// Persistent } diff --git a/Runtime/NetworkVariable/CollectionSerializationUtility.cs b/Runtime/NetworkVariable/CollectionSerializationUtility.cs index 096b2a4..ae8cda6 100644 --- a/Runtime/NetworkVariable/CollectionSerializationUtility.cs +++ b/Runtime/NetworkVariable/CollectionSerializationUtility.cs @@ -505,8 +505,13 @@ public static void WriteNativeListDelta(FastBufferWriter writer, ref NativeLi writer.WriteValueSafe(changes); unsafe { +#if UTP_TRANSPORT_2_0_ABOVE + var ptr = value.GetUnsafePtr(); + var prevPtr = previousValue.GetUnsafePtr(); +#else var ptr = (T*)value.GetUnsafePtr(); var prevPtr = (T*)previousValue.GetUnsafePtr(); +#endif for (int i = 0; i < value.Length; ++i) { if (changes.IsSet(i)) @@ -549,7 +554,11 @@ public static void ReadNativeListDelta(FastBufferReader reader, ref NativeLis unsafe { +#if UTP_TRANSPORT_2_0_ABOVE + var ptr = value.GetUnsafePtr(); +#else var ptr = (T*)value.GetUnsafePtr(); +#endif for (var i = 0; i < value.Length; ++i) { if (changes.IsSet(i)) @@ -571,8 +580,13 @@ public static void ReadNativeListDelta(FastBufferReader reader, ref NativeLis public static unsafe void WriteNativeHashSetDelta(FastBufferWriter writer, ref NativeHashSet value, ref NativeHashSet previousValue) where T : unmanaged, IEquatable { // See WriteHashSet; this is the same algorithm, adjusted for the NativeHashSet API +#if UTP_TRANSPORT_2_0_ABOVE + var added = stackalloc T[value.Count]; + var removed = stackalloc T[previousValue.Count]; +#else var added = stackalloc T[value.Count()]; var removed = stackalloc T[previousValue.Count()]; +#endif var addedCount = 0; var removedCount = 0; foreach (var item in value) @@ -592,8 +606,11 @@ public static unsafe void WriteNativeHashSetDelta(FastBufferWriter writer, re ++removedCount; } } - +#if UTP_TRANSPORT_2_0_ABOVE + if (addedCount + removedCount >= value.Count) +#else if (addedCount + removedCount >= value.Count()) +#endif { writer.WriteByteSafe(1); writer.WriteValueSafe(value); @@ -643,9 +660,15 @@ public static unsafe void WriteNativeHashMapDelta(FastBufferWriter w where TVal : unmanaged { // See WriteDictionary; this is the same algorithm, adjusted for the NativeHashMap API +#if UTP_TRANSPORT_2_0_ABOVE + var added = stackalloc KVPair[value.Count]; + var changed = stackalloc KVPair[value.Count]; + var removed = stackalloc KVPair[previousValue.Count]; +#else var added = stackalloc KeyValue[value.Count()]; var changed = stackalloc KeyValue[value.Count()]; var removed = stackalloc KeyValue[previousValue.Count()]; +#endif var addedCount = 0; var changedCount = 0; var removedCount = 0; @@ -672,8 +695,11 @@ public static unsafe void WriteNativeHashMapDelta(FastBufferWriter w ++removedCount; } } - +#if UTP_TRANSPORT_2_0_ABOVE + if (addedCount + removedCount + changedCount >= value.Count) +#else if (addedCount + removedCount + changedCount >= value.Count()) +#endif { writer.WriteByteSafe(1); writer.WriteValueSafe(value); diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index a489e9d..35c38c8 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -49,6 +49,11 @@ public NetworkList(IEnumerable values = default, } } + ~NetworkList() + { + Dispose(); + } + /// public override void ResetDirty() { @@ -153,6 +158,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++) { @@ -175,7 +187,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -183,7 +195,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; @@ -213,7 +229,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -221,7 +237,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; @@ -247,7 +267,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -255,7 +275,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; @@ -275,7 +299,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -283,7 +307,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; @@ -311,7 +339,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) }); } - if (keepDirtyDelta) + if (isServer) { m_DirtyEvents.Add(new NetworkListEvent() { @@ -320,7 +348,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; @@ -337,13 +369,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; @@ -357,6 +394,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() { @@ -555,8 +604,17 @@ public int LastModifiedTick /// public override void Dispose() { - m_List.Dispose(); - m_DirtyEvents.Dispose(); + if (m_List.IsCreated) + { + m_List.Dispose(); + } + + if (m_DirtyEvents.IsCreated) + { + m_DirtyEvents.Dispose(); + } + + base.Dispose(); } } diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index 938d534..6469c9e 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); } @@ -56,6 +57,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 @@ -65,12 +67,32 @@ public NetworkVariable(T value = default, m_PreviousValue = default; } + /// + /// Resets the NetworkVariable when the associated NetworkObject is not spawned + /// + /// the value to reset the NetworkVariable to (if none specified it resets to the default) + 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; + } + } + /// /// The internal value of the NetworkVariable /// [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; @@ -101,6 +123,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); @@ -121,6 +144,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)) { @@ -151,6 +185,13 @@ public override void Dispose() } m_InternalValue = default; + + if (m_InternalOriginalValue is IDisposable internalOriginalValueDisposable) + { + internalOriginalValueDisposable.Dispose(); + } + m_InternalOriginalValue = default; + if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable) { m_HasPreviousValue = false; @@ -171,6 +212,14 @@ 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. @@ -182,11 +231,12 @@ 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; } @@ -204,6 +254,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(); } @@ -236,28 +288,64 @@ 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); } - 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); } /// @@ -265,5 +353,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 8c7db22..f35a6c2 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -227,6 +227,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 /// @@ -313,6 +319,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/NetworkVariable/NetworkVariableSerialization.cs b/Runtime/NetworkVariable/NetworkVariableSerialization.cs index 70c94cf..2d77e56 100644 --- a/Runtime/NetworkVariable/NetworkVariableSerialization.cs +++ b/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -1728,8 +1728,13 @@ internal static unsafe bool ValueEqualsList(ref NativeList(ref NativeList< { return false; } - +#if UTP_TRANSPORT_2_0_ABOVE + var aptr = a.GetUnsafePtr(); + var bptr = b.GetUnsafePtr(); +#else var aptr = (TValueType*)a.GetUnsafePtr(); var bptr = (TValueType*)b.GetUnsafePtr(); +#endif + for (var i = 0; i < a.Length; ++i) { if (!EqualityEquals(ref aptr[i], ref bptr[i])) @@ -1883,7 +1893,11 @@ internal static bool EqualityEqualsNativeHashSet(ref NativeHashSet a, { return true; } - +#if UTP_TRANSPORT_2_0_ABOVE + if (a.Count != b.Count) +#else if (a.Count() != b.Count()) +#endif { return false; } diff --git a/Runtime/NetworkVariable/ResizableBitVector.cs b/Runtime/NetworkVariable/ResizableBitVector.cs index 5b3ec1e..b4b4b76 100644 --- a/Runtime/NetworkVariable/ResizableBitVector.cs +++ b/Runtime/NetworkVariable/ResizableBitVector.cs @@ -95,11 +95,19 @@ public unsafe void NetworkSerialize(BufferSerializer serializer) where T : { if (serializer.IsReader) { +#if UTP_TRANSPORT_2_0_ABOVE + serializer.GetFastBufferReader().ReadBytesSafe(ptr, length); +#else serializer.GetFastBufferReader().ReadBytesSafe((byte*)ptr, length); +#endif } else { +#if UTP_TRANSPORT_2_0_ABOVE + serializer.GetFastBufferWriter().WriteBytesSafe(ptr, length); +#else serializer.GetFastBufferWriter().WriteBytesSafe((byte*)ptr, length); +#endif } } } diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index aa50ad6..5fb5ca7 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -700,7 +700,7 @@ public unsafe void WriteBytes(byte* value, int size, int offset = 0) } if (Handle->Position + size > Handle->AllowedWriteMark) { - throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}(), Position+Size={Handle->Position + size} > AllowedWriteMark={Handle->AllowedWriteMark}"); } #endif UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); @@ -729,7 +729,7 @@ public unsafe void WriteBytesSafe(byte* value, int size, int offset = 0) if (!TryBeginWriteInternal(size)) { - throw new OverflowException("Writing past the end of the buffer"); + throw new OverflowException($"Writing past the end of the buffer, size is {size} bytes but remaining capacity is {Handle->Capacity - Handle->Position} bytes"); } UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); Handle->Position += size; @@ -772,7 +772,11 @@ public unsafe void WriteBytes(NativeArray value, int size = -1, int offset [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteBytes(NativeList value, int size = -1, int offset = 0) { +#if UTP_TRANSPORT_2_0_ABOVE + byte* ptr = value.GetUnsafePtr(); +#else byte* ptr = (byte*)value.GetUnsafePtr(); +#endif WriteBytes(ptr, size == -1 ? value.Length : size, offset); } @@ -816,7 +820,11 @@ public unsafe void WriteBytesSafe(NativeArray value, int size = -1, int of [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteBytesSafe(NativeList value, int size = -1, int offset = 0) { +#if UTP_TRANSPORT_2_0_ABOVE + byte* ptr = value.GetUnsafePtr(); +#else byte* ptr = (byte*)value.GetUnsafePtr(); +#endif WriteBytesSafe(ptr, size == -1 ? value.Length : size, offset); } @@ -985,7 +993,12 @@ internal unsafe void WriteUnmanagedSafe(NativeArray value) where T : unman internal unsafe void WriteUnmanaged(NativeList value) where T : unmanaged { WriteUnmanaged(value.Length); + +#if UTP_TRANSPORT_2_0_ABOVE + var ptr = value.GetUnsafePtr(); +#else var ptr = (T*)value.GetUnsafePtr(); +#endif { byte* bytes = (byte*)ptr; WriteBytes(bytes, sizeof(T) * value.Length); @@ -995,7 +1008,11 @@ internal unsafe void WriteUnmanaged(NativeList value) where T : unmanaged internal unsafe void WriteUnmanagedSafe(NativeList value) where T : unmanaged { WriteUnmanagedSafe(value.Length); +#if UTP_TRANSPORT_2_0_ABOVE + var ptr = value.GetUnsafePtr(); +#else var ptr = (T*)value.GetUnsafePtr(); +#endif { byte* bytes = (byte*)ptr; WriteBytesSafe(bytes, sizeof(T) * value.Length); @@ -1193,7 +1210,11 @@ public void WriteValue(NativeList value, ForGeneric unused = default) wher [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteValueSafe(NativeHashSet value) where T : unmanaged, IEquatable { +#if UTP_TRANSPORT_2_0_ABOVE + WriteUnmanagedSafe(value.Count); +#else WriteUnmanagedSafe(value.Count()); +#endif foreach (var item in value) { var iReffable = item; @@ -1206,7 +1227,11 @@ internal void WriteValueSafe(NativeHashMap value) where TKey : unmanaged, IEquatable where TVal : unmanaged { +#if UTP_TRANSPORT_2_0_ABOVE + WriteUnmanagedSafe(value.Count); +#else WriteUnmanagedSafe(value.Count()); +#endif foreach (var item in value) { (var key, var val) = (item.Key, item.Value); diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index 7852166..ca7c6d2 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -245,8 +245,30 @@ internal void RemoveOwnership(NetworkObject networkObject) ChangeOwnership(networkObject, NetworkManager.ServerClientId); } + private Dictionary m_LastChangeInOwnership = new Dictionary(); + private const int k_MaximumTickOwnershipChangeMultiplier = 6; + internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) { + // 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 (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.IsServer) { throw new NotServerException("Only the server can change ownership"); @@ -257,22 +279,30 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) throw new SpawnStateException("Object is not spawned"); } - var previous = networkObject.OwnerClientId; + // Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates + networkObject.PreviousOwnerId = networkObject.OwnerClientId; + // Assign the new owner networkObject.OwnerClientId = clientId; // Always notify locally on the server when ownership is lost networkObject.InvokeBehaviourOnLostOwnership(); - networkObject.MarkVariablesDirty(true); - NetworkManager.BehaviourUpdater.AddForUpdate(networkObject); - // Server 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 message = new ChangeOwnershipMessage { NetworkObjectId = networkObject.NetworkObjectId, @@ -292,7 +322,15 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) /// !!Important!! /// 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(previous, clientId); + 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 (!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) @@ -417,14 +455,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 @@ -476,6 +514,8 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow { // Create prefab instance 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; } @@ -1152,7 +1192,24 @@ internal void HandleNetworkObjectShow() ulong clientId = client.Key; foreach (var networkObject in client.Value) { - SendSpawnCallForObject(clientId, networkObject); + // Ignore if null or not spawned (v1.x.x the server should only show what is spawned) + if (networkObject != null && networkObject.IsSpawned) + { + // Prevent exceptions from interrupting this iteration + // so the ObjectsToShowToClient list will be fully processed + // and cleard. + try + { + SendSpawnCallForObject(clientId, networkObject); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } } } ObjectsToShowToClient.Clear(); diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs index 984da63..420bad1 100644 --- a/Runtime/Transports/UTP/UnityTransport.cs +++ b/Runtime/Transports/UTP/UnityTransport.cs @@ -406,6 +406,7 @@ public struct SimulatorParameters #if UTP_TRANSPORT_2_0_ABOVE [Obsolete("DebugSimulator is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)] + [HideInInspector] #endif public SimulatorParameters DebugSimulator = new SimulatorParameters { @@ -1208,6 +1209,30 @@ public override ulong GetCurrentRtt(ulong clientId) return (ulong)ExtractRtt(ParseClientId(clientId)); } + /// + /// Provides the for the NGO client identifier specified. + /// + /// + /// - This is only really useful for direct connections. + /// - Relay connections and clients connected using a distributed authority network topology will not provide the client's actual endpoint information. + /// - For LAN topologies this should work as long as it is a direct connection and not a relay connection. + /// + /// NGO client identifier to get endpoint information about. + /// + public NetworkEndpoint GetEndpoint(ulong clientId) + { + if (m_Driver.IsCreated && NetworkManager != null && NetworkManager.IsListening) + { + var transportId = NetworkManager.ConnectionManager.ClientIdToTransportId(clientId); + var networkConnection = ParseClientId(transportId); + if (m_Driver.GetConnectionState(networkConnection) == NetworkConnection.State.Connected) + { + return m_Driver.RemoteEndPoint(networkConnection); + } + } + return new NetworkEndpoint(); + } + /// /// Initializes the transport /// diff --git a/Samples~/Bootstrap/.sample.json b/Samples~/Bootstrap/.sample.json deleted file mode 100644 index f62cf30..0000000 --- a/Samples~/Bootstrap/.sample.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "displayName": "Bootstrap", - "description": "A lightweight sample to get started" -} \ No newline at end of file diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index a97a92b..e5a9aa6 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -753,6 +753,11 @@ protected void ClientNetworkManagerPostStartInit() protected virtual bool LogAllMessages => false; + protected virtual bool ShouldCheckForSpawnedPlayers() + { + return true; + } + /// /// This starts the server and clients as long as /// returns true. @@ -819,7 +824,10 @@ protected IEnumerator StartServerAndClients() } } - ClientNetworkManagerPostStartInit(); + if (ShouldCheckForSpawnedPlayers()) + { + ClientNetworkManagerPostStartInit(); + } // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. @@ -892,7 +900,10 @@ protected void StartServerAndClientsWithTimeTravel() } } - ClientNetworkManagerPostStartInit(); + if (ShouldCheckForSpawnedPlayers()) + { + ClientNetworkManagerPostStartInit(); + } // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs index bd14cfa..ad6075d 100644 --- a/Tests/Runtime/ConnectionApproval.cs +++ b/Tests/Runtime/ConnectionApproval.cs @@ -1,65 +1,135 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Text; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; -using UnityEngine; using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests { - public class ConnectionApprovalTests + [TestFixture(PlayerCreation.Prefab)] + [TestFixture(PlayerCreation.PrefabHash)] + [TestFixture(PlayerCreation.NoPlayer)] + [TestFixture(PlayerCreation.FailValidation)] + internal class ConnectionApprovalTests : NetcodeIntegrationTest { + private const string k_InvalidToken = "Invalid validation token!"; + public enum PlayerCreation + { + Prefab, + PrefabHash, + NoPlayer, + FailValidation + } + private PlayerCreation m_PlayerCreation; + private bool m_ClientDisconnectReasonValidated; + + private Dictionary m_Validated = new Dictionary(); + + public ConnectionApprovalTests(PlayerCreation playerCreation) + { + m_PlayerCreation = playerCreation; + } + + protected override int NumberOfClients => 1; + private Guid m_ValidationToken; - private bool m_IsValidated; - [SetUp] - public void Setup() + protected override bool ShouldCheckForSpawnedPlayers() + { + return m_PlayerCreation != PlayerCreation.NoPlayer; + } + + protected override void OnServerAndClientsCreated() { - // Create, instantiate, and host - Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _, NetworkManagerHelper.NetworkManagerOperatingMode.None)); + m_ClientDisconnectReasonValidated = false; + m_BypassConnectionTimeout = m_PlayerCreation == PlayerCreation.FailValidation; + m_Validated.Clear(); m_ValidationToken = Guid.NewGuid(); + var validationToken = Encoding.UTF8.GetBytes(m_ValidationToken.ToString()); + m_ServerNetworkManager.ConnectionApprovalCallback = NetworkManagerObject_ConnectionApprovalCallback; + m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerCreation == PlayerCreation.Prefab ? m_PlayerPrefab : null; + if (m_PlayerCreation == PlayerCreation.PrefabHash) + { + m_ServerNetworkManager.NetworkConfig.Prefabs.Add(new NetworkPrefab() { Prefab = m_PlayerPrefab }); + } + m_ServerNetworkManager.NetworkConfig.ConnectionApproval = true; + m_ServerNetworkManager.NetworkConfig.ConnectionData = validationToken; + + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.PlayerPrefab = m_PlayerCreation == PlayerCreation.Prefab ? m_PlayerPrefab : null; + if (m_PlayerCreation == PlayerCreation.PrefabHash) + { + client.NetworkConfig.Prefabs.Add(new NetworkPrefab() { Prefab = m_PlayerPrefab }); + } + client.NetworkConfig.ConnectionApproval = true; + client.NetworkConfig.ConnectionData = m_PlayerCreation == PlayerCreation.FailValidation ? Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) : validationToken; + if (m_PlayerCreation == PlayerCreation.FailValidation) + { + client.OnClientDisconnectCallback += Client_OnClientDisconnectCallback; + } + } + + base.OnServerAndClientsCreated(); } - [UnityTest] - public IEnumerator ConnectionApproval() + private void Client_OnClientDisconnectCallback(ulong clientId) + { + m_ClientNetworkManagers[0].OnClientDisconnectCallback -= Client_OnClientDisconnectCallback; + m_ClientDisconnectReasonValidated = m_ClientNetworkManagers[0].LocalClientId == clientId && m_ClientNetworkManagers[0].DisconnectReason == k_InvalidToken; + } + + private bool ClientAndHostValidated() { - NetworkManagerHelper.NetworkManagerObject.ConnectionApprovalCallback = NetworkManagerObject_ConnectionApprovalCallback; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionApproval = true; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.PlayerPrefab = null; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(m_ValidationToken.ToString()); - m_IsValidated = false; - NetworkManagerHelper.NetworkManagerObject.StartHost(); - - var timeOut = Time.realtimeSinceStartup + 3.0f; - var timedOut = false; - while (!m_IsValidated) + if (!m_Validated.ContainsKey(m_ServerNetworkManager.LocalClientId) || !m_Validated[m_ServerNetworkManager.LocalClientId]) { - yield return new WaitForSeconds(0.01f); - if (timeOut < Time.realtimeSinceStartup) + return false; + } + if (m_PlayerCreation == PlayerCreation.FailValidation) + { + return m_ClientDisconnectReasonValidated; + } + else + { + foreach (var client in m_ClientNetworkManagers) { - timedOut = true; + if (!m_Validated.ContainsKey(client.LocalClientId) || !m_Validated[client.LocalClientId]) + { + return false; + } } } + return true; + } - //Make sure we didn't time out - Assert.False(timedOut); - Assert.True(m_IsValidated); + [UnityTest] + public IEnumerator ConnectionApproval() + { + yield return WaitForConditionOrTimeOut(ClientAndHostValidated); + AssertOnTimeout("Timed out waiting for all clients to be approved!"); } private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) { var stringGuid = Encoding.UTF8.GetString(request.Payload); + if (m_ValidationToken.ToString() == stringGuid) { - m_IsValidated = true; + m_Validated.Add(request.ClientNetworkId, true); + response.Approved = true; + } + else + { + response.Approved = false; + response.Reason = "Invalid validation token!"; } - response.Approved = m_IsValidated; - response.CreatePlayerObject = false; + response.CreatePlayerObject = ShouldCheckForSpawnedPlayers(); response.Position = null; response.Rotation = null; - response.PlayerPrefabHash = null; + response.PlayerPrefabHash = m_PlayerCreation == PlayerCreation.PrefabHash ? m_PlayerPrefab.GetComponent().GlobalObjectIdHash : null; } @@ -78,13 +148,5 @@ public void VerifyUniqueNetworkConfigPerRequest() Assert.True(currentHash != newHash, $"Hashed {nameof(NetworkConfig)} values {currentHash} and {newHash} should not be the same!"); } - - [TearDown] - public void TearDown() - { - // Stop, shutdown, and destroy - NetworkManagerHelper.ShutdownNetworkManager(); - } - } } diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs index f85a9cb..1cccee0 100644 --- a/Tests/Runtime/DeferredMessagingTests.cs +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -667,7 +667,7 @@ public void WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnS serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - WaitForAllClientsToReceive(); + WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -675,9 +675,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); } @@ -809,7 +809,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) @@ -818,10 +818,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/HiddenVariableTests.cs b/Tests/Runtime/HiddenVariableTests.cs index 83d1597..e545b6d 100644 --- a/Tests/Runtime/HiddenVariableTests.cs +++ b/Tests/Runtime/HiddenVariableTests.cs @@ -173,11 +173,11 @@ 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); - Debug.Assert(HiddenVariableObject.SpawnCount == NumberOfClients + 1); + AssertOnTimeout($"Timed out waiting for all clients to spawn {m_NetSpawnedObject.name}"); Debug.Log("Objects spawned"); // ==== Set the NetworkVariable value to 2 @@ -210,7 +210,7 @@ public IEnumerator HiddenVariableTest() Debug.Log("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/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs index ae243cc..64bab78 100644 --- a/Tests/Runtime/Messaging/NamedMessageTests.cs +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs @@ -239,5 +239,45 @@ public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrow }); } } + + [Test] + public unsafe void ErrorMessageIsPrintedWhenAttemptingToSendNamedMessageWithTooBigBuffer() + { + // First try a valid send with the maximum allowed size (this is atm 1264) + var msgSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(ulong)/*MessageName hash*/ - sizeof(NetworkBatchHeader); + var bufferSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize; + var messageName = Guid.NewGuid().ToString(); + var messageContent = new byte[msgSize]; + var writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, new List { FirstClient.LocalClientId }, writer); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, FirstClient.LocalClientId, writer); + } + + msgSize++; + messageContent = new byte[msgSize]; + writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + var message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, new List { FirstClient.LocalClientId }, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + + message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, FirstClient.LocalClientId, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + } + } } } diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs b/Tests/Runtime/Messaging/UnnamedMessageTests.cs index bd583cf..9355115 100644 --- a/Tests/Runtime/Messaging/UnnamedMessageTests.cs +++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs @@ -194,5 +194,44 @@ public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrow }); } } + + [Test] + public unsafe void ErrorMessageIsPrintedWhenAttemptingToSendUnnamedMessageWithTooBigBuffer() + { + // First try a valid send with the maximum allowed size (this is atm 1272) + var msgSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(NetworkBatchHeader); + var bufferSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize; + var messageContent = new byte[msgSize]; + var writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(new List { FirstClient.LocalClientId }, writer); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + } + + msgSize++; + messageContent = new byte[msgSize]; + writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + var message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(new List { FirstClient.LocalClientId }, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + + message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + } + } } } diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs b/Tests/Runtime/NetworkManagerEventsTests.cs index 4cebe20..34a406a 100644 --- a/Tests/Runtime/NetworkManagerEventsTests.cs +++ b/Tests/Runtime/NetworkManagerEventsTests.cs @@ -13,6 +13,61 @@ public 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() { @@ -228,6 +283,9 @@ in NetworkTimeSystem.Sync */ [UnityTearDown] public virtual IEnumerator Teardown() { + NetworkManager.OnInstantiated -= NetworkManager_OnInstantiated; + NetworkManager.OnDestroying -= NetworkManager_OnDestroying; + NetcodeIntegrationTestHelpers.Destroy(); if (m_ServerManager != null) { diff --git a/Tests/Runtime/NetworkManagerTransportTests.cs b/Tests/Runtime/NetworkManagerTransportTests.cs index fa1fbda..a9d8e3a 100644 --- a/Tests/Runtime/NetworkManagerTransportTests.cs +++ b/Tests/Runtime/NetworkManagerTransportTests.cs @@ -1,6 +1,9 @@ using System; using System.Collections; using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport; using UnityEngine; using UnityEngine.TestTools; @@ -157,4 +160,51 @@ public override void DisconnectLocalClient() } } } + + /// + /// Verifies the UnityTransport.GetEndpoint method returns + /// valid NetworkEndPoint information. + /// + internal class TransportEndpointTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + [UnityTest] + public IEnumerator GetEndpointReportedCorrectly() + { + var serverUnityTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as UnityTransport; + var serverEndpoint = new NetworkEndPoint(); + var clientEndpoint = new NetworkEndPoint(); + foreach (var client in m_ClientNetworkManagers) + { + var unityTransport = client.NetworkConfig.NetworkTransport as UnityTransport; + serverEndpoint = unityTransport.GetEndpoint(m_ServerNetworkManager.LocalClientId); + clientEndpoint = serverUnityTransport.GetEndpoint(client.LocalClientId); + Assert.IsTrue(serverEndpoint.IsValid); + Assert.IsTrue(clientEndpoint.IsValid); + Assert.IsTrue(clientEndpoint.Address.Split(":")[0] == unityTransport.ConnectionData.Address); + Assert.IsTrue(serverEndpoint.Address.Split(":")[0] == serverUnityTransport.ConnectionData.Address); + Assert.IsTrue(serverEndpoint.Port == unityTransport.ConnectionData.Port); + Assert.IsTrue(clientEndpoint.Port >= serverUnityTransport.ConnectionData.Port); + } + + // Now validate that when disconnected it returns a non-valid NetworkEndPoint + var clientId = m_ClientNetworkManagers[0].LocalClientId; + m_ClientNetworkManagers[0].Shutdown(); + yield return s_DefaultWaitForTick; + + serverEndpoint = (m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport as UnityTransport).GetEndpoint(m_ServerNetworkManager.LocalClientId); + clientEndpoint = serverUnityTransport.GetEndpoint(clientId); + Assert.IsFalse(serverEndpoint.IsValid); + Assert.IsFalse(clientEndpoint.IsValid); + + // Validate that invalid client identifiers return an invalid NetworkEndPoint + serverEndpoint = (m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport as UnityTransport).GetEndpoint((ulong)UnityEngine.Random.Range(NumberOfClients + 1, 30)); + clientEndpoint = serverUnityTransport.GetEndpoint((ulong)UnityEngine.Random.Range(NumberOfClients + 1, 30)); + Assert.IsFalse(serverEndpoint.IsValid); + Assert.IsFalse(clientEndpoint.IsValid); + } + } + + } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs index 8f35c97..68dc16e 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs @@ -48,7 +48,7 @@ protected override void OnServerAndClientsCreated() [UnityTest] // When this test fails it does so without an exception and will wait the default ~6 minutes - [Timeout(10000)] + [Timeout(360000)] public IEnumerator WhenManyObjectsAreSpawnedAtOnce_AllAreReceived() { for (int x = 0; x < k_SpawnedObjects; x++) diff --git a/Tests/Runtime/NetworkVariableCollectionsTests.cs b/Tests/Runtime/NetworkVariableCollectionsTests.cs index 7d7e785..c0db8f6 100644 --- a/Tests/Runtime/NetworkVariableCollectionsTests.cs +++ b/Tests/Runtime/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,487 @@ public IEnumerator TestHashSetBuiltInTypeCollections() } } + [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(); + } + base.OnServerAndClientsCreated(); + } + + private bool AllInstancesSpawned() + { + 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() + { + 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 (m_UseHost) + { + m_NetworkManagers.Add(m_ServerNetworkManager.LocalClientId, m_ServerNetworkManager); + } + foreach (var client in m_ClientNetworkManagers) + { + m_NetworkManagers.Add(client.LocalClientId, client); + } + + var authorityNetworkManager = !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 (IsServer && !IsOwnershipChanging()) + { + var index = NetworkManager.ConnectedClientsIds.ToList().IndexOf(OwnerClientId); + index++; + index = index % NetworkManager.ConnectedClientsIds.Count; + m_NextClient = NetworkManager.ConnectedClientsIds[index]; + + if (IsOwnedByServer && NetworkManager.IsServer) + { + HelperState = HelperStates.ChangingOwner; + SetTickDelay(); + Log($"Locally changing ownership to Client-{m_NextClient}"); + } + + if (NetworkManager.IsServer && !IsOwnedByServer) + { + // 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 +2505,14 @@ protected override void OnNetworkPostSpawn() ListCollectionServer.OnValueChanged += OnServerListValuesChanged; ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + if (!IsDebugMode) + { + InitValues(); + } + } + + public void InitValues() + { if (IsServer) { ListCollectionServer.Value = OnSetServerValues(); @@ -1660,8 +2524,8 @@ protected override void OnNetworkPostSpawn() ListCollectionOwner.Value = OnSetOwnerValues(); ListCollectionOwner.CheckDirtyState(); } - base.OnNetworkPostSpawn(); } + public override void OnNetworkDespawn() { ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; @@ -1705,12 +2569,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 +3469,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 +3731,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 +3765,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/NetworkVisibilityTests.cs b/Tests/Runtime/NetworkVisibilityTests.cs index bbd9cb4..5d8b651 100644 --- a/Tests/Runtime/NetworkVisibilityTests.cs +++ b/Tests/Runtime/NetworkVisibilityTests.cs @@ -20,6 +20,8 @@ public enum SceneManagementState private GameObject m_TestNetworkPrefab; private bool m_SceneManagementEnabled; + private GameObject m_SpawnedObject; + public NetworkVisibilityTests(SceneManagementState sceneManagementState) { m_SceneManagementEnabled = sceneManagementState == SceneManagementState.SceneManagementEnabled; @@ -40,7 +42,7 @@ protected override void OnServerAndClientsCreated() protected override IEnumerator OnServerAndClientsConnected() { - SpawnObject(m_TestNetworkPrefab, m_ServerNetworkManager); + m_SpawnedObject = SpawnObject(m_TestNetworkPrefab, m_ServerNetworkManager); yield return base.OnServerAndClientsConnected(); } @@ -54,7 +56,43 @@ public IEnumerator HiddenObjectsTest() yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == 2); #endif - Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for the visible object count to equal 2!"); + AssertOnTimeout("Timed out waiting for the visible object count to equal 2!"); + } + + + [UnityTest] + public IEnumerator HideShowAndDeleteTest() + { +#if UNITY_2023_1_OR_NEWER + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == 2); +#else + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == 2); +#endif + AssertOnTimeout("Timed out waiting for the visible object count to equal 2!"); + + var serverNetworkObject = m_SpawnedObject.GetComponent(); + + serverNetworkObject.NetworkHide(m_ClientNetworkManagers[0].LocalClientId); + +#if UNITY_2023_1_OR_NEWER + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == 1); +#else + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == 1); +#endif + AssertOnTimeout($"Timed out waiting for {m_SpawnedObject.name} to be hidden from client!"); + var networkObjectId = serverNetworkObject.NetworkObjectId; + serverNetworkObject.NetworkShow(m_ClientNetworkManagers[0].LocalClientId); + serverNetworkObject.Despawn(true); + + // Expect no exceptions + yield return s_DefaultWaitForTick; + + // Now force a scenario where it normally would have caused an exception + m_ServerNetworkManager.SpawnManager.ObjectsToShowToClient.Add(m_ClientNetworkManagers[0].LocalClientId, new System.Collections.Generic.List()); + m_ServerNetworkManager.SpawnManager.ObjectsToShowToClient[m_ClientNetworkManagers[0].LocalClientId].Add(null); + + // Expect no exceptions + yield return s_DefaultWaitForTick; } } } diff --git a/Tests/Runtime/PlayerObjectTests.cs b/Tests/Runtime/PlayerObjectTests.cs index bd947f2..0fb3b9e 100644 --- a/Tests/Runtime/PlayerObjectTests.cs +++ b/Tests/Runtime/PlayerObjectTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Linq; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; @@ -48,4 +49,112 @@ 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.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; + + var playerObjects = m_ServerNetworkManager.SpawnManager.SpawnedObjectsList.Where((c) => c.IsPlayerObject).ToList(); + + // 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 playerObjects) + { + Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObject.NetworkObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{playerObject.NetworkObjectId}!"); + } + } + } + } + + /// + /// This test validates the player position and rotation is correct + /// relative to the prefab's initial settings if no changes are applied. + /// + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class PlayerSpawnPositionTests : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 2; + + public PlayerSpawnPositionTests(HostOrServer hostOrServer) + { + m_UseHost = hostOrServer == HostOrServer.Host; + } + + 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/Tests/Runtime/UniversalRpcTests.cs b/Tests/Runtime/UniversalRpcTests.cs index e157a3a..b06bb97 100644 --- a/Tests/Runtime/UniversalRpcTests.cs +++ b/Tests/Runtime/UniversalRpcTests.cs @@ -918,6 +918,7 @@ public void RethrowTargetInvocationException(Action action) } } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingNoOverride : UniversalRpcTestsBase @@ -948,6 +949,7 @@ public void TestSendingNoOverride( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSenderClientId : UniversalRpcTestsBase @@ -978,6 +980,7 @@ public void TestSenderClientId( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingNoOverrideWithParams : UniversalRpcTestsBase @@ -1020,6 +1023,7 @@ public void TestSendingNoOverrideWithParams( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingNoOverrideWithParamsAndRpcParams : UniversalRpcTestsBase @@ -1062,6 +1066,7 @@ public void TestSendingNoOverrideWithParamsAndRpcParams( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestRequireOwnership : UniversalRpcTestsBase @@ -1098,6 +1103,7 @@ public void TestRequireOwnership( } } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestDisallowedOverride : UniversalRpcTestsBase @@ -1133,6 +1139,7 @@ public void TestDisallowedOverride( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingWithTargetOverride : UniversalRpcTestsBase @@ -1166,6 +1173,7 @@ public void TestSendingWithTargetOverride( } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingWithSingleOverride : UniversalRpcTestsBase @@ -1213,6 +1221,7 @@ public IEnumerator TestSendingWithSingleOverride() } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingWithSingleNotOverride : UniversalRpcTestsBase @@ -1260,6 +1269,7 @@ public IEnumerator TestSendingWithSingleNotOverride() } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingWithGroupOverride : UniversalRpcTestsBase @@ -1286,81 +1296,59 @@ public enum AllocationType } // Extending timeout since the added yield return causes this test to commonly timeout - [Timeout(600000)] - [UnityTest] - public IEnumerator TestSendingWithGroupOverride() + [Test] + public void TestSendingWithGroupOverride( + [Values] SendTo defaultSendTo, + [ValueSource(nameof(RecipientGroups))] ulong[] recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender, + [Values] AllocationType allocationType + ) { - var waitFor = new WaitForFixedUpdate(); - foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) - { - m_EnableVerboseDebug = true; - VerboseDebug($"Processing: {defaultSendTo}"); - m_EnableVerboseDebug = false; + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - foreach (var recipient in RecipientGroups) - { - for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) + { + case AllocationType.Array: + target = senderObject.RpcTarget.Group(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Group(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Group(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) { - for (ulong sender = 0u; sender <= 2u; ++sender) - { - yield return waitFor; - foreach (var allocationType in Enum.GetValues(typeof(AllocationType))) - { - //if (++YieldCheck % YieldCycleCount == 0) - //{ - // yield return null; - //} - OnInlineSetup(); - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - - var senderObject = GetPlayerObject(objectOwner, sender); - BaseRpcTarget target = null; - switch (allocationType) - { - case AllocationType.Array: - target = senderObject.RpcTarget.Group(recipient, RpcTargetUse.Temp); - break; - case AllocationType.List: - target = senderObject.RpcTarget.Group(recipient.ToList(), RpcTargetUse.Temp); - break; - case AllocationType.NativeArray: - var arr = new NativeArray(recipient, Allocator.Temp); - target = senderObject.RpcTarget.Group(arr, RpcTargetUse.Temp); - arr.Dispose(); - break; - case AllocationType.NativeList: - // For some reason on 2020.3, calling list.AsArray() and passing that to the next function - // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later - // versions of Unity. - var list = new NativeList(recipient.Length, Allocator.TempJob); - foreach (var id in recipient) - { - list.Add(id); - } - - target = senderObject.RpcTarget.Group(list, RpcTargetUse.Temp); - list.Dispose(); - break; - } - - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - - VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient.Contains(c)).ToArray(), false); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); - - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); - OnInlineTearDown(); - } - } + list.Add(id); } - } + target = senderObject.RpcTarget.Group(list, RpcTargetUse.Temp); + list.Dispose(); + break; } + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); } } + + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSendingWithGroupNotOverride : UniversalRpcTestsBase @@ -1386,82 +1374,60 @@ public enum AllocationType List } + // Extending timeout since the added yield return causes this test to commonly timeout - [Timeout(600000)] - [UnityTest] - public IEnumerator TestSendingWithGroupNotOverride() + [Test] + public void TestSendingWithGroupNotOverride( + [Values] SendTo defaultSendTo, + [ValueSource(nameof(RecipientGroups))] ulong[] recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender, + [Values] AllocationType allocationType + ) { - var waitFor = new WaitForFixedUpdate(); - foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) { - m_EnableVerboseDebug = true; - VerboseDebug($"Processing: {defaultSendTo}"); - m_EnableVerboseDebug = false; - foreach (var recipient in RecipientGroups) - { - for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) + case AllocationType.Array: + target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Not(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Not(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) { - for (ulong sender = 0u; sender <= 2u; ++sender) - { - yield return waitFor; - - foreach (var allocationType in Enum.GetValues(typeof(AllocationType))) - { - //if (++YieldCheck % YieldCycleCount == 0) - //{ - // yield return waitFor; - //} - - OnInlineSetup(); - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - - var senderObject = GetPlayerObject(objectOwner, sender); - BaseRpcTarget target = null; - switch (allocationType) - { - case AllocationType.Array: - target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); - break; - case AllocationType.List: - target = senderObject.RpcTarget.Not(recipient.ToList(), RpcTargetUse.Temp); - break; - case AllocationType.NativeArray: - var arr = new NativeArray(recipient, Allocator.Temp); - target = senderObject.RpcTarget.Not(arr, RpcTargetUse.Temp); - arr.Dispose(); - break; - case AllocationType.NativeList: - // For some reason on 2020.3, calling list.AsArray() and passing that to the next function - // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later - // versions of Unity. - var list = new NativeList(recipient.Length, Allocator.TempJob); - foreach (var id in recipient) - { - list.Add(id); - } - target = senderObject.RpcTarget.Not(list, RpcTargetUse.Temp); - list.Dispose(); - break; - } - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - - VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray(), false); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); - - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); - OnInlineTearDown(); - } - } + list.Add(id); } - } + target = senderObject.RpcTarget.Not(list, RpcTargetUse.Temp); + list.Dispose(); + break; } - } + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + } } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestDefaultSendToSpecifiedInParamsSendingToServerAndOwner : UniversalRpcTestsBase @@ -1472,6 +1438,7 @@ public UniversalRpcTestDefaultSendToSpecifiedInParamsSendingToServerAndOwner(Hos } } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestDeferLocal : UniversalRpcTestsBase @@ -1659,6 +1626,7 @@ public IEnumerator TestDeferLocalOverrideToFalse() } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestMutualRecursion : UniversalRpcTestsBase @@ -1708,6 +1676,7 @@ public void TestMutualRecursion() } + [Timeout(1200000)] [TestFixture(HostOrServer.Host)] [TestFixture(HostOrServer.Server)] internal class UniversalRpcTestSelfRecursion : UniversalRpcTestsBase @@ -1747,6 +1716,7 @@ public void TestSelfRecursion() } + [Timeout(1200000)] [TestFixture(ObjType.Server)] [TestFixture(ObjType.Client)] internal class UniversalRpcTestRpcTargetUse : UniversalRpcTestsBase diff --git a/ValidationExceptions.json b/ValidationExceptions.json deleted file mode 100644 index 2d81612..0000000 --- a/ValidationExceptions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ErrorExceptions": [ - { - "ValidationTest": "API Validation", - "ExceptionMessage": "Additions require a new minor or major version.", - "PackageVersion": "1.5.2" - } - ], - "WarningExceptions": [] -} \ No newline at end of file diff --git a/ValidationExceptions.json.meta b/ValidationExceptions.json.meta deleted file mode 100644 index 3316cf2..0000000 --- a/ValidationExceptions.json.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2a43005be301c9043aab7034757d4868 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/package.json b/package.json index a67ca47..6f69f5a 100644 --- a/package.json +++ b/package.json @@ -2,29 +2,29 @@ "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": "1.11.0", + "version": "1.12.0", "unity": "2021.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", "com.unity.transport": "1.4.0" }, + "samples": [ + { + "displayName": "Bootstrap", + "description": "A lightweight sample to get started", + "path": "Samples~/Bootstrap" + } + ], "_upm": { - "changelog": "### Added\n\n- Added `NetworkVariable.CheckDirtyState` that is to be used in tandem with collections in order to detect whether the collection or an item within the collection has changed. (#3005)\n\n### Fixed\n\n- Fixed issue by adding null checks in `NetworkVariableBase.CanClientRead` and `NetworkVariableBase.CanClientWrite` methods to ensure safe access to `NetworkBehaviour`. (#3011)\n- Fixed issue using collections within `NetworkVariable` where the collection would not detect changes to items or nested items. (#3005)\n- Fixed issue where `List`, `Dictionary`, and `HashSet` collections would not uniquely duplicate nested collections. (#3005)\n- Fixed Issue where a state with dual triggers, inbound and outbound, could cause a false layer to layer state transition message to be sent to non-authority `NetworkAnimator` instances and cause a warning message to be logged. (#2999)\n- Fixed issue where `FixedStringSerializer` was using `NetworkVariableSerialization.AreEqual` to determine if two bytes were equal causes an exception to be thrown due to no byte serializer having been defined. (#2992)\n\n### Changed\n\n- Changed permissions exception thrown in `NetworkList` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3005)\n- Changed permissions exception thrown in `NetworkVariable.Value` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3005)" + "changelog": "### Added\n\n- Added `UnityTransport.GetEndpoint` method to provide a way to obtain `NetworkEndpoint` information of a connection via client identifier. (#3131)\n- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3089)\n- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3089)\n- Added message size validation to named and unnamed message sending functions for better error messages. (#3043)\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. (#3034)\n\n### Fixed\n\n- Fixed issue where `NetworkList` properties on in-scene placed `NetworkObject`s could cause small memory leaks when entering playmode. (#3148)\n- Fixed issue where a newly synchronizing client would be synchronized with the current `NetworkVariable` values always which could cause issues with collections if there were any pending state updates. Now, when initially synchronizing a client, if a `NetworkVariable` has a pending state update it will serialize the previously known value(s) to the synchronizing client so when the pending updates are sent they aren't duplicate values on the newly connected client side. (#3126)\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. (#3126)\n- Fixed issue with `NetworkVariable` collections where transferring ownership to another client would not update the new owner's previous value to the most current value which could cause the last/previous added value to be detected as a change when adding or removing an entry (as long as the entry removed was not the last/previously added value). (#3126)\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. (#3126)\n- Fixed issue where `NetworkAnimator` would statically allocate write buffer space for `Animator` parameters that could cause a write error if the number of parameters exceeded the space allocated. (#3124)\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. (#3084)\n- Fixed issue where `NetworkAnimator` would send updates to non-observer clients. (#3058)\n- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3055)\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. (#3046)\n- Fixed issue where collections v2.2.x was not supported when using UTP v2.2.x within Unity v2022.3. (#3033)\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. (#3029)\n- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3027)\n\n### Changed\n\n- Changed `NetworkVariableDeltaMessage` so the server now forwards delta state updates (owner write permission based from a client) to other clients immediately as opposed to keeping a `NetworkVariable` or `NetworkList` dirty and processing them at the end of the frame or potentially on the next network tick. (#3126)\n- The Debug Simulator section of the Unity Transport component will now be hidden if Unity Transport 2.0 or later is installed. It was already non-functional in that situation and users should instead use the more featureful [Network Sim" }, "upmCi": { - "footprint": "aa624034952045f7f2399c1f99fd31b764234959" + "footprint": "2c48e079ced3767ca83dee44470f99ea4d866a02" }, - "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.11/manual/index.html", + "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.12/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "e3303ba66b4a642ccf0bc72104107e1b8e1ebe1c" - }, - "samples": [ - { - "displayName": "Bootstrap", - "description": "A lightweight sample to get started", - "path": "Samples~/Bootstrap" - } - ] + "revision": "109ededce37b86fd2e51c7c53f128fe1503eaef5" + } }