From 83d8030be39bd6f20b61dcfe5b1b93315386c41d Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 19 Sep 2025 17:38:38 -0400 Subject: [PATCH 1/3] Add NetworkList getter and redo NetworkList tests --- .../Collections/NetworkList.cs | 74 ++-- .../NetworkVariable/NetworkListTests.cs | 410 ++++++++++++++++++ .../NetworkVariable/NetworkListTests.cs.meta | 3 + .../NetworkVariable/NetworkVariableTests.cs | 377 ---------------- 4 files changed, 458 insertions(+), 406 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index c7c2554d21..c4b6a0e803 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Unity.Collections; namespace Unity.Netcode @@ -597,45 +598,60 @@ public void RemoveAt(int index) } /// - /// Gets or sets the element at the specified index in the . + /// Sets the element at the specified index in the . /// /// - /// This method checks for write permissions before setting the value. + /// This method checks for write permissions and equality before setting and updating the value. /// - /// The zero-based index of the element to get or set. - /// The element at the specified index. - public T this[int index] + /// The zero-based index of the element to set. + /// The new value to set at the given index + /// + /// Ignores the equality check when setting the value. + /// This option can send unnecessary updates to all clients when the value hasn't changed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index, T value, bool forceUpdate = false) { - get => m_List[index]; - set + // check write permissions + if (CannotWrite) { - // check write permissions - if (CannotWrite) - { - LogWritePermissionError(); - return; - } + LogWritePermissionError(); + return; + } - var previousValue = m_List[index]; + var previousValue = m_List[index]; - // Only trigger an event if the value has changed - if (NetworkVariableSerialization.AreEqual(ref previousValue, ref value)) - { - return; - } + // Only trigger an event if the value has changed + if (!forceUpdate && NetworkVariableSerialization.AreEqual(ref previousValue, ref value)) + { + return; + } - m_List[index] = value; + m_List[index] = value; - var listEvent = new NetworkListEvent() - { - Type = NetworkListEvent.EventType.Value, - Index = index, - Value = value, - PreviousValue = previousValue - }; + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Value, + Index = index, + Value = value, + PreviousValue = previousValue + }; - HandleAddListEvent(listEvent); - } + HandleAddListEvent(listEvent); + } + + /// + /// Gets or sets the element at the specified index in the . + /// + /// + /// This method checks for write permissions before setting the value. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + public T this[int index] + { + get => m_List[index]; + set => Set(index, value); } /// diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs new file mode 100644 index 0000000000..15c737fbc3 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs @@ -0,0 +1,410 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; +using Random = UnityEngine.Random; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] + [TestFixture(HostOrServer.Server)] + internal class NetworkListTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 3; + + public NetworkListTests(HostOrServer host) : base(host) { } + + private GameObject m_ListObjectPrefab; + + private List m_ExpectedValues = new(); + + private ulong m_TestObjectId; + + protected override void OnServerAndClientsCreated() + { + m_ListObjectPrefab = CreateNetworkObjectPrefab("ListObject"); + m_ListObjectPrefab.AddComponent(); + + base.OnServerAndClientsCreated(); + } + + private bool OnVerifyData(StringBuilder errorLog) + { + foreach (var manager in m_NetworkManagers) + { + if (!manager.SpawnManager.SpawnedObjects.TryGetValue(m_TestObjectId, out NetworkObject networkObject)) + { + errorLog.Append($"[Client-{manager.LocalClientId}] Test object was not spawned"); + return false; + } + + var listComponent = networkObject.GetComponent(); + if (listComponent == null) + { + errorLog.Append($"[Client-{manager.LocalClientId}] List component was not found"); + return false; + } + + if (m_ExpectedValues.Count != listComponent.TheList.Count) + { + errorLog.Append($"[Client-{manager.LocalClientId}] List component has the incorrect number of items. Expected: {m_ExpectedValues.Count}, Have: {listComponent.TheList.Count}"); + return false; + } + + for (int i = 0; i < m_ExpectedValues.Count; i++) + { + var expectedValue = m_ExpectedValues[i]; + var actual = listComponent.TheList[i]; + + if (expectedValue != actual) + { + errorLog.Append($"[Client-{manager.LocalClientId}] Incorrect value at index {i}, expected: {expectedValue}, actual: {actual}"); + return false; + } + } + } + + return true; + } + + [UnityTest] + public IEnumerator ValidateNetworkListSynchronization() + { + var authority = GetAuthorityNetworkManager(); + var nonAuthority = GetNonAuthorityNetworkManager(); + var instantiatedObject = SpawnObject(m_ListObjectPrefab, authority).GetComponent(); + m_TestObjectId = instantiatedObject.NetworkObjectId; + + yield return WaitForSpawnedOnAllOrTimeOut(instantiatedObject); + AssertOnTimeout("[Setup] Failed to spawn list object"); + + Assert.IsTrue(nonAuthority.SpawnManager.SpawnedObjects.TryGetValue(instantiatedObject.NetworkObjectId, out var nonAuthorityObject)); + + var authorityInstance = instantiatedObject.GetComponent(); + var nonAuthorityInstance = nonAuthorityObject.GetComponent(); + + m_ExpectedValues.Clear(); + + // NetworkList.Add + for (int i = 0; i < 10; i++) + { + var val = Random.Range(0, 1234); + m_ExpectedValues.Add(val); + + authorityInstance.TheList.Add(val); + } + + yield return WaitForConditionOrTimeOut(OnVerifyData); + AssertOnTimeout("[Add] Failed to add items to list"); + + // NetworkList.Contains + foreach (var expectedValue in m_ExpectedValues) + { + Assert.IsTrue(authorityInstance.TheList.Contains(expectedValue), $"[Contains][Client-{authority.LocalClientId}] Does not contain {expectedValue}"); + Assert.IsTrue(nonAuthorityInstance.TheList.Contains(expectedValue), $"[Contains][Client-{nonAuthority.LocalClientId}] Does not contain {expectedValue}"); + } + + // NetworkList.Insert + for (int i = 0; i < 5; i++) + { + var indexToInsert = Random.Range(0, m_ExpectedValues.Count); + var valToInsert = Random.Range(1, 99); + m_ExpectedValues.Insert(indexToInsert, valToInsert); + + authorityInstance.TheList.Insert(indexToInsert, valToInsert); + } + + yield return WaitForConditionOrTimeOut(OnVerifyData); + AssertOnTimeout("[Insert] Failed to insert items to list"); + + + // NetworkList.IndexOf + foreach (var testValue in Shuffle(m_ExpectedValues)) + { + var expectedIndex = m_ExpectedValues.IndexOf(testValue); + + Assert.AreEqual(expectedIndex, authorityInstance.TheList.IndexOf(testValue), $"[IndexOf][Client-{authority.LocalClientId}] Has incorrect index for {testValue}"); + Assert.AreEqual(expectedIndex, nonAuthorityInstance.TheList.IndexOf(testValue), $"[IndexOf][Client-{nonAuthority.LocalClientId}] Has incorrect index for {testValue}"); + } + + // NetworkList[index] getter and setter + foreach (var testValue in Shuffle(m_ExpectedValues)) + { + var testIndex = m_ExpectedValues.IndexOf(testValue); + + // Set up our original and previous + var previousValue = testValue; + var updatedValue = previousValue + 10; + + m_ExpectedValues[testIndex] = updatedValue; + + Assert.AreEqual(testValue, authorityInstance.TheList[testIndex], $"[Get][Client-{authority.LocalClientId}] incorrect index get"); + Assert.AreEqual(testValue, nonAuthorityInstance.TheList[testIndex], $"[Get][Client-{nonAuthority.LocalClientId}] incorrect index get"); + + var callbackSucceeded = false; + + // Callback that verifies the changed event occurred and that the original and new values are correct + void TestValueUpdatedCallback(NetworkListEvent changedEvent) + { + nonAuthorityInstance.TheList.OnListChanged -= TestValueUpdatedCallback; + + callbackSucceeded = changedEvent.PreviousValue == previousValue && + changedEvent.Value == updatedValue; + } + + // Subscribe to the OnListChanged event on the client side and + nonAuthorityInstance.TheList.OnListChanged += TestValueUpdatedCallback; + authorityInstance.TheList[testIndex] = updatedValue; + + yield return WaitForConditionOrTimeOut(() => callbackSucceeded); + AssertOnTimeout($"[OnListChanged][Client-{nonAuthority.LocalClientId}] client callback was not called"); + } + + yield return WaitForConditionOrTimeOut(OnVerifyData); + AssertOnTimeout("[NetworkList[index]] Failed to get/set items at index in list"); + + /* + * NetworkList.Set with same value (forced and non-forced updates) + */ + var expectedUpdateCount = 0; + var actualUpdateCount = 0; + + // Callback that verifies the changed event occurred and that the original and new values are correct + void TestForceUpdateCallback(NetworkListEvent _) + { + actualUpdateCount++; + } + nonAuthorityInstance.TheList.OnListChanged += TestForceUpdateCallback; + + foreach (var testValue in Shuffle(m_ExpectedValues)) + { + var testIndex = m_ExpectedValues.IndexOf(testValue); + var forceUpdate = testIndex % 2 == 0; + if (forceUpdate) + { + expectedUpdateCount++; + } + // Subscribe to the OnListChanged event on the client side and + authorityInstance.TheList.Set(testIndex, testValue, forceUpdate); + } + + yield return WaitForConditionOrTimeOut(() => actualUpdateCount == expectedUpdateCount); + AssertOnTimeout($"[OnListChanged][Client-{nonAuthority.LocalClientId}] OnListChanged update was called an incorrect number of times"); + nonAuthorityInstance.TheList.OnListChanged -= TestForceUpdateCallback; + + /* + * NetworkList.Remove and NetworkList.RemoveAt + */ + foreach (var testValue in Shuffle(m_ExpectedValues)) + { + var testIndex = m_ExpectedValues.IndexOf(testValue); + + // Add a new value to the end to ensure the list isn't depleted + var newValue = Random.Range(0, 99); + m_ExpectedValues.Add(newValue); + authorityInstance.TheList.Add(newValue); + + var removeAt = testIndex % 2 == 0; + if (removeAt) + { + m_ExpectedValues.RemoveAt(testIndex); + authorityInstance.TheList.RemoveAt(testIndex); + } + else + { + m_ExpectedValues.Remove(testValue); + authorityInstance.TheList.Remove(testValue); + } + } + yield return WaitForConditionOrTimeOut(OnVerifyData); + AssertOnTimeout($"[Remove] List is incorrect after removing items"); + + /* + * NetworkList.Clear + */ + m_ExpectedValues.Clear(); + authorityInstance.TheList.Clear(); + + yield return WaitForConditionOrTimeOut(OnVerifyData); + AssertOnTimeout($"[Clear] List is incorrect after clearing items"); + } + + // don't extend this please + [UnityTest] + public IEnumerator LegacyPredicateTesting() + { + var authority = GetAuthorityNetworkManager(); + var nonAuthority = GetNonAuthorityNetworkManager(); + + var instantiatedObject = SpawnObject(m_ListObjectPrefab, authority).GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(instantiatedObject); + AssertOnTimeout("Failed to spawn list object"); + + Assert.IsTrue(nonAuthority.SpawnManager.SpawnedObjects.TryGetValue(instantiatedObject.NetworkObjectId, out var nonAuthorityObject)); + + var authorityInstance = instantiatedObject.GetComponent(); + var nonAuthorityInstance = nonAuthorityObject.GetComponent(); + + // WhenListContainsManyLargeValues_OverflowExceptionIsNotThrown + var overflowPredicate = new NetworkListTestPredicate(authorityInstance, nonAuthorityInstance, 20); + yield return WaitForConditionOrTimeOut(overflowPredicate); + AssertOnTimeout("Overflow exception shouldn't be thrown when adding many large values"); + + /* + * NetworkList Struct + */ + bool VerifyList() + { + return nonAuthorityInstance.TheStructList.Count == authorityInstance.TheStructList.Count && + nonAuthorityInstance.TheStructList[0].Value == authorityInstance.TheStructList[0].Value && + nonAuthorityInstance.TheStructList[1].Value == authorityInstance.TheStructList[1].Value; + } + + authorityInstance.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 1 }); + authorityInstance.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 2 }); + authorityInstance.TheStructList.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyList); + AssertOnTimeout("All list values should match between clients"); + } + + private int[] Shuffle(List list) + { + var rng = new System.Random(); + + // Order the list by a progression of random numbers + // This will do a shuffle of the list + return list.OrderBy(_ => rng.Next()).ToArray(); + } + } + + internal class NetworkListTest : NetworkBehaviour + { + public readonly NetworkList TheList = new(); + public readonly NetworkList TheStructList = new(); + public readonly NetworkList TheLargeList = new(); + + private void ListChanged(NetworkListEvent e) + { + ListDelegateTriggered = true; + } + + public void Awake() + { + TheList.OnListChanged += ListChanged; + } + + public override void OnDestroy() + { + TheList.OnListChanged -= ListChanged; + base.OnDestroy(); + } + + public bool ListDelegateTriggered; + } + + + /// + /// Handles the more generic conditional logic for NetworkList tests + /// which can be used with the NetcodeIntegrationTest.WaitForConditionOrTimeOut + /// that accepts anything derived from the class + /// as a parameter. + /// + internal class NetworkListTestPredicate : ConditionalPredicateBase + { + private readonly NetworkListTest m_AuthorityInstance; + + private readonly NetworkListTest m_NonAuthorityInstance; + + private string m_TestStageFailedMessage; + + /// + /// Determines if the condition has been reached for the current NetworkListTestState + /// + protected override bool OnHasConditionBeenReached() + { + return OnContainsLarge() && OnVerifyData(); + } + + /// + /// Provides all information about the players for both sides for simplicity and informative sake. + /// + /// + private string ConditionFailedInfo() + { + return $"[ContainsLarge] condition test failed:\n Server List Count: {m_AuthorityInstance.TheList.Count} vs Client List Count: {m_NonAuthorityInstance.TheList.Count}\n" + + $"Server List Count: {m_AuthorityInstance.TheLargeList.Count} vs Client List Count: {m_NonAuthorityInstance.TheLargeList.Count}\n" + + $"Server Delegate Triggered: {m_AuthorityInstance.ListDelegateTriggered} | Client Delegate Triggered: {m_NonAuthorityInstance.ListDelegateTriggered}\n"; + } + + /// + /// When finished, check if a time out occurred and if so assert and provide meaningful information to troubleshoot why + /// + protected override void OnFinished() + { + Assert.IsFalse(TimedOut, $"{nameof(NetworkListTestPredicate)} timed out waiting for the ContainsLarge condition to be reached! \n" + ConditionFailedInfo()); + } + + // Uses the ArrayOperator and validates that on both sides the count and values are the same + private bool OnVerifyData() + { + // Wait until both sides have the same number of elements + if (m_AuthorityInstance.TheLargeList.Count != m_NonAuthorityInstance.TheLargeList.Count) + { + return false; + } + + // Check the client values against the server values to make sure they match + for (int i = 0; i < m_AuthorityInstance.TheLargeList.Count; i++) + { + if (m_AuthorityInstance.TheLargeList[i] != m_NonAuthorityInstance.TheLargeList[i]) + { + return false; + } + } + return true; + } + + /// + /// The current version of this test only verified the count of the large list, so that is what this does + /// + private bool OnContainsLarge() + { + return m_AuthorityInstance.TheLargeList.Count == m_NonAuthorityInstance.TheLargeList.Count && OnVerifyData(); + } + + private const string k_CharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static string GenerateRandomString(int length) + { + var charArray = k_CharSet.Distinct().ToArray(); + var result = new char[length]; + for (int i = 0; i < length; i++) + { + result[i] = charArray[RandomNumberGenerator.GetInt32(charArray.Length)]; + } + + return new string(result); + } + + public NetworkListTestPredicate(NetworkListTest authorityInstance, NetworkListTest nonAuthorityInstance, int elementCount) + { + m_AuthorityInstance = authorityInstance; + m_NonAuthorityInstance = nonAuthorityInstance; + + for (var i = 0; i < elementCount; ++i) + { + m_AuthorityInstance.TheLargeList.Add(new FixedString128Bytes(GenerateRandomString(Random.Range(0, 99)))); + } + } + } + +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs.meta new file mode 100644 index 0000000000..2cdfab2387 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9834036b76954617b9e09312903b2754 +timeCreated: 1758219317 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableTests.cs index 351b6ab9fd..06ef4969d0 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableTests.cs @@ -8,7 +8,6 @@ using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.TestTools; -using Random = UnityEngine.Random; namespace Unity.Netcode.RuntimeTests { @@ -112,29 +111,15 @@ public enum SomeEnum } public readonly NetworkVariable TheScalar = new NetworkVariable(); public readonly NetworkVariable TheEnum = new NetworkVariable(); - public readonly NetworkList TheList = new NetworkList(); - public readonly NetworkList TheStructList = new NetworkList(); - public readonly NetworkList TheLargeList = new NetworkList(); public readonly NetworkVariable FixedString32 = new NetworkVariable(); - private void ListChanged(NetworkListEvent e) - { - ListDelegateTriggered = true; - } - - public void Awake() - { - TheList.OnListChanged += ListChanged; - } - public readonly NetworkVariable TheStruct = new NetworkVariable(); public readonly NetworkVariable TheClass = new NetworkVariable(); public NetworkVariable> TheTemplateStruct = new NetworkVariable>(); public NetworkVariable> TheTemplateClass = new NetworkVariable>(); - public bool ListDelegateTriggered; public override void OnNetworkSpawn() { @@ -146,177 +131,6 @@ public override void OnNetworkSpawn() } } - /// - /// Handles the more generic conditional logic for NetworkList tests - /// which can be used with the - /// that accepts anything derived from the class - /// as a parameter. - /// - internal class NetworkListTestPredicate : ConditionalPredicateBase - { - private const int k_MaxRandomValue = 1000; - - private Dictionary> m_StateFunctions; - - // Player1 component on the Server - private NetworkVariableTest m_Player1OnServer; - - // Player1 component on client1 - private NetworkVariableTest m_Player1OnClient1; - - private string m_TestStageFailedMessage; - - public enum NetworkListTestStates - { - Add, - ContainsLarge, - Contains, - VerifyData, - IndexOf, - } - - private NetworkListTestStates m_NetworkListTestState; - - public void SetNetworkListTestState(NetworkListTestStates networkListTestState) - { - m_NetworkListTestState = networkListTestState; - } - - /// - /// Determines if the condition has been reached for the current NetworkListTestState - /// - protected override bool OnHasConditionBeenReached() - { - var isStateRegistered = m_StateFunctions.ContainsKey(m_NetworkListTestState); - Assert.IsTrue(isStateRegistered); - return m_StateFunctions[m_NetworkListTestState].Invoke(); - } - - /// - /// Provides all information about the players for both sides for simplicity and informative sake. - /// - /// - private string ConditionFailedInfo() - { - return $"{m_NetworkListTestState} condition test failed:\n Server List Count: {m_Player1OnServer.TheList.Count} vs Client List Count: {m_Player1OnClient1.TheList.Count}\n" + - $"Server List Count: {m_Player1OnServer.TheLargeList.Count} vs Client List Count: {m_Player1OnClient1.TheLargeList.Count}\n" + - $"Server Delegate Triggered: {m_Player1OnServer.ListDelegateTriggered} | Client Delegate Triggered: {m_Player1OnClient1.ListDelegateTriggered}\n"; - } - - /// - /// When finished, check if a time out occurred and if so assert and provide meaningful information to troubleshoot why - /// - protected override void OnFinished() - { - Assert.IsFalse(TimedOut, $"{nameof(NetworkListTestPredicate)} timed out waiting for the {m_NetworkListTestState} condition to be reached! \n" + ConditionFailedInfo()); - } - - // Uses the ArrayOperator and validates that on both sides the count and values are the same - private bool OnVerifyData() - { - // Wait until both sides have the same number of elements - if (m_Player1OnServer.TheList.Count != m_Player1OnClient1.TheList.Count) - { - return false; - } - - // Check the client values against the server values to make sure they match - for (int i = 0; i < m_Player1OnServer.TheList.Count; i++) - { - if (m_Player1OnServer.TheList[i] != m_Player1OnClient1.TheList[i]) - { - return false; - } - } - return true; - } - - /// - /// Verifies the data count, values, and that the ListDelegate on both sides was triggered - /// - private bool OnAdd() - { - bool wasTriggerred = m_Player1OnServer.ListDelegateTriggered && m_Player1OnClient1.ListDelegateTriggered; - return wasTriggerred && OnVerifyData(); - } - - /// - /// The current version of this test only verified the count of the large list, so that is what this does - /// - private bool OnContainsLarge() - { - return m_Player1OnServer.TheLargeList.Count == m_Player1OnClient1.TheLargeList.Count; - } - - /// - /// Tests NetworkList.Contains which also verifies all values are the same on both sides - /// - private bool OnContains() - { - // Wait until both sides have the same number of elements - if (m_Player1OnServer.TheList.Count != m_Player1OnClient1.TheList.Count) - { - return false; - } - - // Parse through all server values and use the NetworkList.Contains method to check if the value is in the list on the client side - foreach (var serverValue in m_Player1OnServer.TheList) - { - if (!m_Player1OnClient1.TheList.Contains(serverValue)) - { - return false; - } - } - return true; - } - - /// - /// Tests NetworkList.IndexOf and verifies that all values are aligned on both sides - /// - private bool OnIndexOf() - { - foreach (var serverSideValue in m_Player1OnServer.TheList) - { - var indexToTest = m_Player1OnServer.TheList.IndexOf(serverSideValue); - if (indexToTest != m_Player1OnServer.TheList.IndexOf(serverSideValue)) - { - return false; - } - } - return true; - } - - public NetworkListTestPredicate(NetworkVariableTest player1OnServer, NetworkVariableTest player1OnClient1, NetworkListTestStates networkListTestState, int elementCount) - { - m_NetworkListTestState = networkListTestState; - m_Player1OnServer = player1OnServer; - m_Player1OnClient1 = player1OnClient1; - m_StateFunctions = new Dictionary> - { - { NetworkListTestStates.Add, OnAdd }, - { NetworkListTestStates.ContainsLarge, OnContainsLarge }, - { NetworkListTestStates.Contains, OnContains }, - { NetworkListTestStates.VerifyData, OnVerifyData }, - { NetworkListTestStates.IndexOf, OnIndexOf } - }; - - if (networkListTestState == NetworkListTestStates.ContainsLarge) - { - for (var i = 0; i < elementCount; ++i) - { - m_Player1OnServer.TheLargeList.Add(new FixedString128Bytes()); - } - } - else - { - for (int i = 0; i < elementCount; i++) - { - m_Player1OnServer.TheList.Add(Random.Range(0, k_MaxRandomValue)); - } - } - } - } - internal class NetvarDespawnShutdown : NetworkBehaviour { private NetworkVariable m_IntNetworkVariable = new NetworkVariable(); @@ -415,8 +229,6 @@ internal class NetworkVariableTests : NetcodeIntegrationTest private const uint k_TestUInt = 0x12345678; private const int k_TestVal1 = 111; - private const int k_TestVal2 = 222; - private const int k_TestVal3 = 333; protected override bool m_EnableTimeTravel => true; protected override bool m_SetupIsACoroutine => false; @@ -434,8 +246,6 @@ public static void ClientNetworkVariableTestSpawned(NetworkVariableTest networkV // Player1 component on client1 private NetworkVariableTest m_Player1OnClient1; - private NetworkListTestPredicate m_NetworkListPredicateHandler; - private readonly bool m_EnsureLengthSafety; public NetworkVariableTests(Serialization serialization) @@ -520,17 +330,6 @@ private void InitializeServerAndClients(HostOrServer useHost) m_Player1OnServer = authority; m_Player1OnClient1 = nonAuthority; - m_Player1OnServer.TheList.Clear(); - - if (m_Player1OnServer.TheList.Count > 0) - { - throw new Exception("at least one server network container not empty at start"); - } - if (m_Player1OnClient1.TheList.Count > 0) - { - throw new Exception("at least one client network container not empty at start"); - } - var instanceCount = useHost == HostOrServer.Server ? NumberOfClients * 2 : NumberOfClients * 3; // Wait for the client-side to notify it is finished initializing and spawning. success = WaitForConditionOrTimeOutWithTimeTravel(() => s_ClientNetworkVariableTestInstances.Count == instanceCount); @@ -621,161 +420,6 @@ public void FixedString32Test([Values] HostOrServer useHost) Assert.True(success, "Timed out waiting for client-side NetworkVariable to update!"); } - [Test] - public void NetworkListAdd([Values] HostOrServer useHost) - { - InitializeServerAndClients(useHost); - m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.Add, 10); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - - [Test] - public void WhenListContainsManyLargeValues_OverflowExceptionIsNotThrown([Values] HostOrServer useHost) - { - InitializeServerAndClients(useHost); - m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.ContainsLarge, 20); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - - [Test] - public void NetworkListContains([Values] HostOrServer useHost) - { - // Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated - NetworkListAdd(useHost); - - // Now test the NetworkList.Contains method - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.Contains); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - - - [Test] - public void NetworkListInsert([Values] HostOrServer useHost) - { - // Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated - NetworkListAdd(useHost); - - // Now randomly insert a random value entry - m_Player1OnServer.TheList.Insert(Random.Range(0, 9), Random.Range(1, 99)); - - // Verify the element count and values on the client matches the server - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - - [Test] - public void NetworkListIndexOf([Values] HostOrServer useHost) - { - // Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated - NetworkListAdd(useHost); - - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.IndexOf); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - - [Test] - public void NetworkListValueUpdate([Values] HostOrServer useHost) - { - var testSucceeded = false; - InitializeServerAndClients(useHost); - // Add 1 element value and verify it is the same on the client - m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.Add, 1); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - - // Setup our original and - var previousValue = m_Player1OnServer.TheList[0]; - var updatedValue = previousValue + 10; - - // Callback that verifies the changed event occurred and that the original and new values are correct - void TestValueUpdatedCallback(NetworkListEvent changedEvent) - { - testSucceeded = changedEvent.PreviousValue == previousValue && - changedEvent.Value == updatedValue; - } - - // Subscribe to the OnListChanged event on the client side and - m_Player1OnClient1.TheList.OnListChanged += TestValueUpdatedCallback; - m_Player1OnServer.TheList[0] = updatedValue; - - // Wait until we know the client side matches the server side before checking if the callback was a success - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData); - WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler); - - Assert.That(testSucceeded); - m_Player1OnClient1.TheList.OnListChanged -= TestValueUpdatedCallback; - } - - private List m_ExpectedValuesServer = new List(); - private List m_ExpectedValuesClient = new List(); - - public enum ListRemoveTypes - { - Remove, - RemoveAt - } - - - [Test] - public void NetworkListRemoveTests([Values] HostOrServer useHost, [Values] ListRemoveTypes listRemoveType) - { - m_ExpectedValuesServer.Clear(); - m_ExpectedValuesClient.Clear(); - // Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated - NetworkListAdd(useHost); - - // Randomly remove a few entries - m_Player1OnServer.TheList.OnListChanged += Server_OnListChanged; - m_Player1OnClient1.TheList.OnListChanged += Client_OnListChanged; - - // Remove half of the elements - for (int i = 0; i < (int)(m_Player1OnServer.TheList.Count * 0.5f); i++) - { - var index = Random.Range(0, m_Player1OnServer.TheList.Count - 1); - var value = m_Player1OnServer.TheList[index]; - m_ExpectedValuesServer.Add(value); - m_ExpectedValuesClient.Add(value); - - if (listRemoveType == ListRemoveTypes.RemoveAt) - { - m_Player1OnServer.TheList.RemoveAt(index); - } - else - { - m_Player1OnServer.TheList.Remove(value); - } - } - - // Verify the element count and values on the client matches the server - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - - Assert.True(m_ExpectedValuesServer.Count == 0, $"Server was not notified of all elements removed and still has {m_ExpectedValuesServer.Count} elements left!"); - Assert.True(m_ExpectedValuesClient.Count == 0, $"Client was not notified of all elements removed and still has {m_ExpectedValuesClient.Count} elements left!"); - } - - private void Server_OnListChanged(NetworkListEvent changeEvent) - { - Assert.True(m_ExpectedValuesServer.Contains(changeEvent.Value)); - m_ExpectedValuesServer.Remove(changeEvent.Value); - } - - private void Client_OnListChanged(NetworkListEvent changeEvent) - { - Assert.True(m_ExpectedValuesClient.Contains(changeEvent.Value)); - m_ExpectedValuesClient.Remove(changeEvent.Value); - } - - [Test] - public void NetworkListClear([Values] HostOrServer useHost) - { - // Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated - NetworkListAdd(useHost); - m_Player1OnServer.TheList.Clear(); - // Verify the element count and values on the client matches the server - m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData); - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler)); - } - [Test] public void TestNetworkVariableClass([Values] HostOrServer useHost) { @@ -813,26 +457,6 @@ bool VerifyClass() Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass)); } - [Test] - public void TestNetworkListStruct([Values] HostOrServer useHost) - { - InitializeServerAndClients(useHost); - - bool VerifyList() - { - return m_Player1OnClient1.TheStructList.Count == m_Player1OnServer.TheStructList.Count && - m_Player1OnClient1.TheStructList[0].Value == m_Player1OnServer.TheStructList[0].Value && - m_Player1OnClient1.TheStructList[1].Value == m_Player1OnServer.TheStructList[1].Value; - } - - m_Player1OnServer.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 1 }); - m_Player1OnServer.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 2 }); - m_Player1OnServer.TheStructList.SetDirty(true); - - // Wait for the client-side to notify it is finished initializing and spawning. - Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyList)); - } - [Test] public void TestNetworkVariableStruct([Values] HostOrServer useHost) { @@ -5256,7 +4880,6 @@ protected override IEnumerator OnTearDown() { Time.timeScale = m_OriginalTimeScale; - m_NetworkListPredicateHandler = null; yield return base.OnTearDown(); } } From 756e33cdda9db97ff7d1932d16457bdc6dffde62 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 19 Sep 2025 17:45:01 -0400 Subject: [PATCH 2/3] update CHANGELOG --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index c3c3d9f73e..17d674e3dd 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -11,6 +11,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added - Clicking on the Help icon in the inspector will now redirect to the relevant documentation. (#3663) +- Added a `Set` function onto `NetworkList` that takes an optional parameter that forces an update to be processed even if the current value is equal to the previous value. (#3690) ### Changed From 8af490785d5806cb25fa2b8cb519a2a044df316b Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 19 Sep 2025 17:47:31 -0400 Subject: [PATCH 3/3] Bump minor version as we are adding new API surface --- com.unity.netcode.gameobjects/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index 2d5ff27d22..bafc5b9943 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -2,7 +2,7 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "2.5.2", + "version": "2.6.0", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", @@ -15,4 +15,4 @@ "path": "Samples~/Bootstrap" } ] -} \ No newline at end of file +}