using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using NUnit.Framework; using Unity.Collections; using Unity.Netcode.TestHelpers.Runtime; namespace Unity.Netcode.RuntimeTests { public class NetworkObjectSceneSerializationTests { /// /// The purpose behind this test is to assure that in-scene NetworkObjects /// that are serialized into a single stream (approval or switch scene this happens) /// will continue to be processed even if one of the NetworkObjects is invalid. /// [Test] public void NetworkObjectSceneSerializationFailure() { var networkObjectsToTest = new List(); var writer = new FastBufferWriter(1300, Allocator.Temp, 4096000); var invalidNetworkObjectOffsets = new List(); var invalidNetworkObjectIdCount = new List(); var invalidNetworkObjects = new List(); var invalidNetworkObjectFrequency = 3; using (writer) { // Construct 50 NetworkObjects for (int i = 0; i < 50; i++) { // Inject an invalid NetworkObject every [invalidNetworkObjectFrequency] entry if ((i % invalidNetworkObjectFrequency) == 0) { // Create the invalid NetworkObject var gameObject = new GameObject($"InvalidTestObject{i}"); Assert.IsNotNull(gameObject); var networkObject = gameObject.AddComponent(); Assert.IsNotNull(networkObject); var networkVariableComponent = gameObject.AddComponent(); Assert.IsNotNull(networkVariableComponent); // Add invalid NetworkObject's starting position before serialization to handle trapping for the Debug.LogError message // that we know will be thrown invalidNetworkObjectOffsets.Add(writer.Position); networkObject.GlobalObjectIdHash = (uint)(i); invalidNetworkObjectIdCount.Add(i); invalidNetworkObjects.Add(gameObject); writer.WriteValueSafe((int)networkObject.GetSceneOriginHandle()); // Serialize the invalid NetworkObject var sceneObject = networkObject.GetMessageSceneObject(0); var prePosition = writer.Position; sceneObject.Serialize(writer); Debug.Log( $"Invalid {nameof(NetworkObject)} Size {writer.Position - prePosition}"); // Now adjust how frequent we will inject invalid NetworkObjects invalidNetworkObjectFrequency = Random.Range(2, 5); } else { // Create a valid NetworkObject var gameObject = new GameObject($"TestObject{i}"); Assert.IsNotNull(gameObject); var networkObject = gameObject.AddComponent(); var networkVariableComponent = gameObject.AddComponent(); Assert.IsNotNull(networkVariableComponent); Assert.IsNotNull(networkObject); networkObject.GlobalObjectIdHash = (uint)(i + 4096); networkObjectsToTest.Add(gameObject); writer.WriteValueSafe(networkObject.GetSceneOriginHandle()); // Handle populating the scenes loaded list var scene = networkObject.gameObject.scene; if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenesLoaded.ContainsKey( scene.handle)) { NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenesLoaded .Add(scene.handle, scene); } var handle = networkObject.GetSceneOriginHandle(); // Since this is a unit test, we will fake the server to client handle lookup by just adding the same handle key and value if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ServerSceneHandleToClientSceneHandle .ContainsKey(handle)) { NetworkManagerHelper.NetworkManagerObject.SceneManager.ServerSceneHandleToClientSceneHandle .Add(handle, handle); } // Serialize the valid NetworkObject var sceneObject = networkObject.GetMessageSceneObject(0); sceneObject.Serialize(writer); if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenePlacedObjects.ContainsKey( networkObject.GlobalObjectIdHash)) { NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenePlacedObjects.Add( networkObject.GlobalObjectIdHash, new Dictionary()); } // Add this valid NetworkObject into the ScenePlacedObjects list NetworkManagerHelper.NetworkManagerObject.SceneManager .ScenePlacedObjects[networkObject.GlobalObjectIdHash] .Add(SceneManager.GetActiveScene().handle, networkObject); } } var totalBufferSize = writer.Position; var reader = new FastBufferReader(writer, Allocator.Temp); using (reader) { var networkObjectsDeSerialized = new List(); var currentLogLevel = NetworkManager.Singleton.LogLevel; var invalidNetworkObjectCount = 0; while (reader.Position != totalBufferSize) { // If we reach the point where we expect it to fail, then make sure we let TestRunner know it should expect this log error message if (invalidNetworkObjectOffsets.Count > 0 && reader.Position == invalidNetworkObjectOffsets[0]) { invalidNetworkObjectOffsets.RemoveAt(0); // Turn off Network Logging to avoid other errors that we know will happen after the below LogAssert.Expect message occurs. NetworkManager.Singleton.LogLevel = LogLevel.Nothing; // Trap for this specific error message so we don't make Test Runner think we failed (it will fail on Debug.LogError) UnityEngine.TestTools.LogAssert.Expect(LogType.Error, $"Failed to spawn {nameof(NetworkObject)} for Hash {invalidNetworkObjectIdCount[invalidNetworkObjectCount]}."); invalidNetworkObjectCount++; } reader.ReadValueSafe(out int handle); NetworkManagerHelper.NetworkManagerObject.SceneManager.SetTheSceneBeingSynchronized(handle); var sceneObject = new NetworkObject.SceneObject(); sceneObject.Deserialize(reader); var deserializedNetworkObject = NetworkObject.AddSceneObject(sceneObject, reader, NetworkManagerHelper.NetworkManagerObject); if (deserializedNetworkObject != null) { networkObjectsDeSerialized.Add(deserializedNetworkObject); } else { // Under this condition, we are expecting null (i.e. no NetworkObject instantiated) // and will set our log level back to the original value to assure the valid NetworkObjects // aren't causing any log Errors to occur NetworkManager.Singleton.LogLevel = currentLogLevel; } } // Now validate all NetworkObjects returned against the original NetworkObjects we created // after they validate, destroy the objects foreach (var entry in networkObjectsToTest) { var entryNetworkObject = entry.GetComponent(); Assert.IsTrue(networkObjectsDeSerialized.Contains(entryNetworkObject)); Object.Destroy(entry); } } } // Destroy the invalid network objects foreach (var entry in invalidNetworkObjects) { Object.Destroy(entry); } } [SetUp] public void Setup() { // Create, instantiate, and host NetworkManagerHelper.StartNetworkManager(out NetworkManager networkManager, NetworkManagerHelper.NetworkManagerOperatingMode.None); networkManager.NetworkConfig.EnableSceneManagement = true; networkManager.StartHost(); } [TearDown] public void TearDown() { // Stop, shutdown, and destroy NetworkManagerHelper.ShutdownNetworkManager(); } } /// /// A simple test class that will provide varying NetworkBuffer stream sizes /// when the NetworkVariable is serialized /// public class NetworkBehaviourWithNetworkVariables : NetworkBehaviour { private const uint k_MinDataBlocks = 1; private const uint k_MaxDataBlocks = 64; public NetworkList NetworkVariableData; private void Awake() { var dataBlocksAssigned = new List(); var numberDataBlocks = Random.Range(k_MinDataBlocks, k_MaxDataBlocks); for (var i = 0; i < numberDataBlocks; i++) { dataBlocksAssigned.Add((ulong)Random.Range(0.0f, float.MaxValue)); } NetworkVariableData = new NetworkList(dataBlocksAssigned); } } }