From 2333218fef0229d1269715ccba6537dc9265160d Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 14:33:52 -0500 Subject: [PATCH 01/12] fix Fixing some of the collections issues. Adding a unified permissions error. Replacing throw exceptions with logging errors and returning. --- .../Collections/NetworkList.cs | 18 ++++--- .../NetworkVariable/NetworkVariable.cs | 49 ++++++++++++++----- .../NetworkVariable/NetworkVariableBase.cs | 29 ++++++++++- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index a082b4e929..85cc6fd5e0 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -369,7 +369,8 @@ public void Add(T item) // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } m_List.Add(item); @@ -390,7 +391,8 @@ public void Clear() // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } m_List.Clear(); @@ -416,7 +418,8 @@ public bool Remove(T item) // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } int index = m_List.IndexOf(item); @@ -451,7 +454,8 @@ public void Insert(int index, T item) // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } if (index < m_List.Length) @@ -480,7 +484,8 @@ public void RemoveAt(int index) // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } var value = m_List[index]; @@ -505,7 +510,8 @@ public T this[int index] // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return; } var previousValue = m_List[index]; diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs index a17b70333d..1ce94b733a 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs @@ -84,20 +84,45 @@ public virtual T Value get => m_InternalValue; set { - // Compare bitwise - if (NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value)) + if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId)) { + LogWritePermissionError(); return; } - if (m_NetworkBehaviour && !CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + // Compare the Value being applied to the current value + if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value) || + // For collections, this will compare the items of the current vs previous to check for changes + !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkVariable"); + Set(value); + m_IsDisposed = false; } + } + } - Set(value); + /// + /// Invoke this method to check if a managed collection's items are different. If so, mark the NetworkVariable dirty. + /// + /// when true, this check will force a full item collection check even if the NetworkVariable is already dirty + /// + /// This is to be used as a way to check if a containing a managed collection has any changees to the collection items. + /// The default behavior is to exit early if the is already dirty. + /// You can set to true to force it to alawys check, even if dirty, in the event you want to be triggered when already dirty. + /// + public bool CheckDirtyState(bool forceCheck = false) + { + var isDirty = base.IsDirty(); + + // Compare the previous with the current if not dirty or forcing a check. + if ((!isDirty || forceCheck) && !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue)) + { + SetDirty(true); + OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); m_IsDisposed = false; + isDirty = true; } + return isDirty; } internal ref T RefValue() @@ -184,9 +209,8 @@ public override void ResetDirty() private protected void Set(T value) { SetDirty(true); - T previousValue = m_InternalValue; m_InternalValue = value; - OnValueChanged?.Invoke(previousValue, m_InternalValue); + OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); } /// @@ -205,20 +229,21 @@ 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. + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + 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 - - T previousValue = m_InternalValue; - NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue); - if (keepDirtyDelta) { SetDirty(true); } - OnValueChanged?.Invoke(previousValue, m_InternalValue); + OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); } /// diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs index 22028dd6b1..96ae64cb6c 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -32,21 +32,48 @@ public abstract class NetworkVariableBase : IDisposable /// Maintains a link to the associated NetworkBehaviour /// private protected NetworkBehaviour m_NetworkBehaviour; + private NetworkManager m_InternalNetworkManager; public NetworkBehaviour GetBehaviour() { return m_NetworkBehaviour; } + internal string GetWritePermissionError() + { + var networkManager = m_NetworkManager == null ? NetworkManager.Singleton : m_NetworkManager; + return $"|Client-{networkManager.LocalClientId}|{m_NetworkBehaviour.name}|{Name}| Write permissions ({WritePerm}) for this client instance is not allowed!"; + } + + internal void LogWritePermissionError() + { + Debug.LogError(GetWritePermissionError()); + } + + private protected NetworkManager m_NetworkManager + { + get + { + if (m_InternalNetworkManager == null && m_NetworkBehaviour && m_NetworkBehaviour.NetworkObject?.NetworkManager) + { + m_InternalNetworkManager = m_NetworkBehaviour.NetworkObject?.NetworkManager; + } + return m_InternalNetworkManager; + } + } + /// /// Initializes the NetworkVariable /// /// The NetworkBehaviour the NetworkVariable belongs to public void Initialize(NetworkBehaviour networkBehaviour) { + m_InternalNetworkManager = null; m_NetworkBehaviour = networkBehaviour; - if (m_NetworkBehaviour.NetworkManager) + if (m_NetworkBehaviour && m_NetworkBehaviour.NetworkObject?.NetworkManager) { + m_InternalNetworkManager = m_NetworkBehaviour.NetworkObject?.NetworkManager; + if (m_NetworkBehaviour.NetworkManager.NetworkTimeSystem != null) { UpdateLastSentTime(); From c5a04127fd5c6238bdcbbafd0e83e1be93441243 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 14:34:34 -0500 Subject: [PATCH 02/12] tesat adjustments for replacing throw exception and added internal unified permissions write error. --- .../Tests/Runtime/NetworkVariableTests.cs | 10 +-- .../Tests/Runtime/OwnerPermissionTests.cs | 68 ++++++------------- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs index b3cef36faa..2029fa1e05 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs @@ -254,7 +254,8 @@ public IEnumerator ClientCannotChangeServerWritableNetVar() var oldValue = testCompClient.ServerWritable_Position.Value; var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); - Assert.That(() => testCompClient.ServerWritable_Position.Value = newValue, Throws.TypeOf()); + LogAssert.Expect(LogType.Error, testCompClient.ServerWritable_Position.GetWritePermissionError()); + testCompClient.ServerWritable_Position.Value = newValue; yield return WaitForPositionsAreEqual(testCompServer.ServerWritable_Position, oldValue); yield return WaitForServerWritableAreEqualOnAll(); @@ -283,7 +284,8 @@ public IEnumerator ServerCannotChangeOwnerWritableNetVar() var oldValue = testCompServer.OwnerWritable_Position.Value; var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); - Assert.That(() => testCompServer.OwnerWritable_Position.Value = newValue, Throws.TypeOf()); + LogAssert.Expect(LogType.Error, testCompServer.OwnerWritable_Position.GetWritePermissionError()); + testCompServer.OwnerWritable_Position.Value = newValue; yield return WaitForPositionsAreEqual(testCompServer.OwnerWritable_Position, oldValue); yield return WaitForOwnerWritableAreEqualOnAll(); @@ -589,8 +591,8 @@ public void ClientWritePermissionTest([Values] HostOrServer useHost) { InitializeServerAndClients(useHost); - // client must not be allowed to write to a server auth variable - Assert.Throws(() => m_Player1OnClient1.TheScalar.Value = k_TestVal1); + LogAssert.Expect(LogType.Error, m_Player1OnClient1.TheScalar.GetWritePermissionError()); + m_Player1OnClient1.TheScalar.Value = k_TestVal1; } /// diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs index 7bd281644b..9db1b0b40a 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs @@ -130,72 +130,44 @@ public IEnumerator OwnerPermissionTest() for (var clientWriting = 0; clientWriting < 3; clientWriting++) { // ==== Server-writable NetworkVariable ==== - var gotException = false; - Debug.Log($"Writing to server-write variable on object {objectIndex} on client {clientWriting}"); + VerboseDebug($"Writing to server-write variable on object {objectIndex} on client {clientWriting}"); - try + nextValueToWrite++; + if (clientWriting != serverIndex) { - nextValueToWrite++; - OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableServer.Value = nextValueToWrite; + LogAssert.Expect(LogType.Error, OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableServer.GetWritePermissionError()); } - catch (Exception) - { - gotException = true; - } - - // Verify server-owned netvar can only be written by server - Debug.Assert(gotException == (clientWriting != serverIndex)); + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableServer.Value = nextValueToWrite; // ==== Owner-writable NetworkVariable ==== - gotException = false; - Debug.Log($"Writing to owner-write variable on object {objectIndex} on client {clientWriting}"); + VerboseDebug($"Writing to owner-write variable on object {objectIndex} on client {clientWriting}"); - try + nextValueToWrite++; + if (clientWriting != objectIndex) { - nextValueToWrite++; - OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableOwner.Value = nextValueToWrite; + LogAssert.Expect(LogType.Error, OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableOwner.GetWritePermissionError()); } - catch (Exception) - { - gotException = true; - } - - // Verify client-owned netvar can only be written by owner - Debug.Assert(gotException == (clientWriting != objectIndex)); + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableOwner.Value = nextValueToWrite; // ==== Server-writable NetworkList ==== - gotException = false; - Debug.Log($"Writing to server-write list on object {objectIndex} on client {clientWriting}"); + VerboseDebug($"Writing to [Add] server-write NetworkList on object {objectIndex} on client {clientWriting}"); - try + nextValueToWrite++; + if (clientWriting != serverIndex) { - nextValueToWrite++; - OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListServer.Add(nextValueToWrite); + LogAssert.Expect(LogType.Error, OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListServer.GetWritePermissionError()); } - catch (Exception) - { - gotException = true; - } - - // Verify server-owned networkList can only be written by server - Debug.Assert(gotException == (clientWriting != serverIndex)); + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListServer.Add(nextValueToWrite); // ==== Owner-writable NetworkList ==== - gotException = false; - Debug.Log($"Writing to owner-write list on object {objectIndex} on client {clientWriting}"); + VerboseDebug($"Writing to [Add] owner-write NetworkList on object {objectIndex} on client {clientWriting}"); - try + nextValueToWrite++; + if (clientWriting != objectIndex) { - nextValueToWrite++; - OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListOwner.Add(nextValueToWrite); + LogAssert.Expect(LogType.Error, OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListOwner.GetWritePermissionError()); } - catch (Exception) - { - gotException = true; - } - - // Verify client-owned networkList can only be written by owner - Debug.Assert(gotException == (clientWriting != objectIndex)); + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListOwner.Add(nextValueToWrite); yield return WaitForTicks(m_ServerNetworkManager, 5); yield return WaitForTicks(m_ClientNetworkManagers[0], 5); From 523d8db8a2f832179a58ac7b4b81f5d67ab0b5ba Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 17:03:45 -0500 Subject: [PATCH 03/12] fix Adjustments made due to other issues with direct assignment. --- .../NetworkVariable/NetworkVariable.cs | 23 ++++++++++++------- .../NetworkVariable/NetworkVariableBase.cs | 3 +-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs index 1ce94b733a..7219ddeb25 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs @@ -79,6 +79,12 @@ public NetworkVariable(T value = default, /// /// The value of the NetworkVariable container /// + /// + /// When assigning collections to , unless it is a completely new collection this will not + /// detect any deltas with most managed collection classes since assignment of one collection value to another + /// is actually just a reference to the collection itself.
+ /// To detect deltas in a collection, you should invoke after making modifications to the collection. + ///
public virtual T Value { get => m_InternalValue; @@ -91,24 +97,25 @@ public virtual T Value } // Compare the Value being applied to the current value - if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value) || - // For collections, this will compare the items of the current vs previous to check for changes - !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue)) + if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value)) { - Set(value); + T previousValue = m_InternalValue; + m_InternalValue = value; + SetDirty(true); m_IsDisposed = false; + OnValueChanged?.Invoke(previousValue, m_InternalValue); } } } /// - /// Invoke this method to check if a managed collection's items are different. If so, mark the NetworkVariable dirty. + /// Invoke this method to check if a collection's items are dirty. + /// The default behavior is to exit early if the is already dirty. /// /// when true, this check will force a full item collection check even if the NetworkVariable is already dirty /// - /// This is to be used as a way to check if a containing a managed collection has any changees to the collection items. - /// The default behavior is to exit early if the is already dirty. - /// You can set to true to force it to alawys check, even if dirty, in the event you want to be triggered when already dirty. + /// This is to be used as a way to check if a containing a managed collection has any changees to the collection items.
+ /// If you invoked this when a collection is dirty, it will not trigger the unless you set to true.
///
public bool CheckDirtyState(bool forceCheck = false) { diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs index 96ae64cb6c..33f5fffb62 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -41,8 +41,7 @@ public NetworkBehaviour GetBehaviour() internal string GetWritePermissionError() { - var networkManager = m_NetworkManager == null ? NetworkManager.Singleton : m_NetworkManager; - return $"|Client-{networkManager.LocalClientId}|{m_NetworkBehaviour.name}|{Name}| Write permissions ({WritePerm}) for this client instance is not allowed!"; + return $"|Client-{m_NetworkManager.LocalClientId}|{m_NetworkBehaviour.name}|{Name}| Write permissions ({WritePerm}) for this client instance is not allowed!"; } internal void LogWritePermissionError() From bf4e74b99ba07c269a1b72bd0500838c06501bcc Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 17:09:52 -0500 Subject: [PATCH 04/12] fix back port typo. --- .../Runtime/NetworkVariable/Collections/NetworkList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index 85cc6fd5e0..a489e9dbff 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -419,7 +419,7 @@ public bool Remove(T item) if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { LogWritePermissionError(); - return; + return false; } int index = m_List.IndexOf(item); From d643a788fc5aef79aa1c47247ccbd550e27e7654 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 17:17:15 -0500 Subject: [PATCH 05/12] style removing using directive. --- .../Tests/Runtime/OwnerPermissionTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs index 9db1b0b40a..0d8f36e0bd 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/OwnerPermissionTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using Unity.Netcode.TestHelpers.Runtime; From e1391a34a8fe1c26259a29a2334b5d238335fc50 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 10 Aug 2024 20:46:08 -0500 Subject: [PATCH 06/12] fix Mark that we have a previous value for disposing when destroyed. --- .../Runtime/NetworkVariable/NetworkVariable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs index 7219ddeb25..938d53401b 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs @@ -238,6 +238,7 @@ 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); NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue); From 6f7833965298602e0b78722991f12167c95164ba Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 12 Aug 2024 14:41:03 -0500 Subject: [PATCH 07/12] fix This fixes an issue with duplicating nested lists, hashsets, and dictionaries. --- .../NetworkVariableSerialization.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs index cda34a57e2..70c94cf3bb 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -349,7 +349,10 @@ public void Duplicate(in List value, ref List duplicatedValue) duplicatedValue.Clear(); foreach (var item in value) { - duplicatedValue.Add(item); + // This handles the nested list scenario List> + T subValue = default; + NetworkVariableSerialization.Duplicate(item, ref subValue); + duplicatedValue.Add(subValue); } } } @@ -421,6 +424,9 @@ public void Duplicate(in HashSet value, ref HashSet duplicatedValue) duplicatedValue.Clear(); foreach (var item in value) { + // Handles nested HashSets + T subValue = default; + NetworkVariableSerialization.Duplicate(item, ref subValue); duplicatedValue.Add(item); } } @@ -497,7 +503,12 @@ public void Duplicate(in Dictionary value, ref Dictionary.Duplicate(item.Key, ref subKey); + NetworkVariableSerialization.Duplicate(item.Value, ref subValue); + duplicatedValue.Add(subKey, subValue); } } } From 6565a3effcac4af79060a4f9281b992d545d8d9c Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 12 Aug 2024 14:42:22 -0500 Subject: [PATCH 08/12] test This is the first set of tests for Lists, including nested, that test using base types and INetworkSerializable. Still have HashSet and Dictionary to cover. --- .../NetworkVariableCollectionsTests.cs | 1787 +++++++++++++++++ .../NetworkVariableCollectionsTests.cs.meta | 11 + 2 files changed, 1798 insertions(+) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs new file mode 100644 index 0000000000..da80cdde41 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs @@ -0,0 +1,1787 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.TestTools; +using Random = UnityEngine.Random; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host, CollectionTypes.List)] + [TestFixture(HostOrServer.Server, CollectionTypes.List)] + public class NetworkVariableCollectionsTests : NetcodeIntegrationTest + { + public enum CollectionTypes + { + Dictionary, + List, + } + + protected override int NumberOfClients => 2; + + private CollectionTypes m_CollectionType; + + public NetworkVariableCollectionsTests(HostOrServer hostOrServer, CollectionTypes collectionType) : base(hostOrServer) + { + m_CollectionType = collectionType; + } + + protected override IEnumerator OnSetup() + { + ListTestHelperInt.ResetState(); + ListTestHelperListInt.ResetState(); + ListTestHelperSerializableObject.ResetState(); + ListTestHelperListSerializableObject.ResetState(); + return base.OnSetup(); + } + + protected override void OnCreatePlayerPrefab() + { + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + base.OnCreatePlayerPrefab(); + } + + private List GetRandomIntList(int count) + { + var list = new List(); + for (int i = 0; i < count; i++) + { + list.Add(Random.Range(int.MinValue, int.MaxValue)); + } + return list; + } + + [UnityTest] + public IEnumerator TestListBuiltInTypeCollections() + { + var compInt = (ListTestHelperInt)null; + var compListInt = (ListTestHelperListInt)null; + var compIntServer = (ListTestHelperInt)null; + var compListIntServer = (ListTestHelperListInt)null; + + var clientList = m_ClientNetworkManagers.ToList(); + if (m_ServerNetworkManager.IsHost) + { + clientList.Insert(0, m_ServerNetworkManager); + } + + foreach (var client in clientList) + { + /////////////////////////////////////////////////////////////////////////// + // List Single dimension list + compInt = client.LocalClient.PlayerObject.GetComponent(); + compIntServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!"); + var randomInt = Random.Range(int.MinValue, int.MaxValue); + + ////////////////////////////////// + // 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}!"); + ////////////////////////////////// + // 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); + var valueIntRemove = compInt.ListCollectionOwner.Value[index]; + compInt.Remove(valueIntRemove, ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + ////////////////////////////////// + // Server Remove int + compIntServer.Remove(valueIntRemove, ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!"); + + //////////////////////////////////// + // Owner Change int + var valueIntChange = Random.Range(int.MinValue, int.MaxValue); + 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 + compIntServer.ListCollectionServer.Value[index] = valueIntChange; + compIntServer.ListCollectionServer.CheckDirtyState(); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + + //////////////////////////////////// + // Owner Add Range + compInt.AddRange(GetRandomIntList(5), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add range failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + ////////////////////////////////// + // Server Add Range + compIntServer.AddRange(GetRandomIntList(5), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add range failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!"); + + //////////////////////////////////// + // Owner Full Set + compInt.FullSet(GetRandomIntList(5), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + ////////////////////////////////// + // Server Full Set + compIntServer.FullSet(GetRandomIntList(5), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + + //////////////////////////////////// + // Owner Clear + compInt.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!"); + ////////////////////////////////// + // Server Clear + compIntServer.Clear(ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!"); + + /////////////////////////////////////////////////////////////////////////// + // List> Nested List Validation + compListInt = client.LocalClient.PlayerObject.GetComponent(); + compListIntServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compListInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListInt.name} component match! {compListInt.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListIntServer.name} component match! {compListIntServer.GetLog()}"); + + ////////////////////////////////// + // Owner Add List item + compListInt.Add(GetRandomIntList(5), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + ////////////////////////////////// + // Server Add List item + compListIntServer.Add(GetRandomIntList(5), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + ////////////////////////////////// + // Owner Remove List item + index = Random.Range(0, compListInt.ListCollectionOwner.Value.Count - 1); + 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); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + + yield return WaitForConditionOrTimeOut(() => compListInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListInt.name} component match! {compListInt.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListIntServer.name} component match! {compListIntServer.GetLog()}"); + + //////////////////////////////////// + // Owner Change List item + index = Random.Range(0, compListInt.ListCollectionOwner.Value.Count - 1); + compListInt.ListCollectionOwner.Value[index] = GetRandomIntList(5); + compListInt.ListCollectionOwner.CheckDirtyState(); + Assert.True(compListInt.ListCollectionOwner.IsDirty(), "Client Should be dirty!"); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} change index ({index}) failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + + ////////////////////////////////// + // Server Change List item + index = Random.Range(0, compListIntServer.ListCollectionServer.Value.Count - 1); + compListIntServer.ListCollectionServer.Value[index] = GetRandomIntList(5); + compListIntServer.ListCollectionServer.CheckDirtyState(); + Assert.True(compListIntServer.ListCollectionServer.IsDirty(), "Server Should be dirty!"); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + //////////////////////////////////// + // Owner Add Range of List items + var randomintListOfList = new List>(); + for (int i = 0; i < 5; i++) + { + randomintListOfList.Add(GetRandomIntList(5)); + } + compListInt.AddRange(randomintListOfList, ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add range failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + ////////////////////////////////// + // Server Add Range of List items + randomintListOfList = new List>(); + for (int i = 0; i < 5; i++) + { + randomintListOfList.Add(GetRandomIntList(5)); + } + compListIntServer.AddRange(randomintListOfList, ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add range failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compListIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListIntServer.name} component match!"); + + //////////////////////////////////// + // Owner Full Set List> + randomintListOfList = new List>(); + for (int i = 0; i < 5; i++) + { + randomintListOfList.Add(GetRandomIntList(5)); + } + compListInt.FullSet(randomintListOfList, ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + ////////////////////////////////// + // Server Full Set List> + randomintListOfList = new List>(); + for (int i = 0; i < 5; i++) + { + randomintListOfList.Add(GetRandomIntList(5)); + } + compListIntServer.FullSet(randomintListOfList, ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + //////////////////////////////////// + // Owner Clear List> + compListInt.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}"); + ////////////////////////////////// + // Server Clear List> + compListIntServer.Clear(ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListInt.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListInt.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListInt.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compListIntServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compListIntServer.name} component match!"); + } + } + + [UnityTest] + public IEnumerator TestListSerializableObjectCollections() + { + var compObject = (ListTestHelperSerializableObject)null; + var compObjectServer = (ListTestHelperSerializableObject)null; + var compListObject = (ListTestHelperListSerializableObject)null; + var compListObjectServer = (ListTestHelperListSerializableObject)null; + + var clientList = m_ClientNetworkManagers.ToList(); + if (m_ServerNetworkManager.IsHost) + { + clientList.Insert(0, m_ServerNetworkManager); + } + + foreach (var client in clientList) + { + /////////////////////////////////////////////////////////////////////////// + // List Single dimension list + compObject = client.LocalClient.PlayerObject.GetComponent(); + compObjectServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compObject.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!"); + + ////////////////////////////////// + // Owner Add SerializableObject + compObject.Add(SerializableObject.GetRandomObject(), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); + ////////////////////////////////// + // Server Add SerializableObject + compObjectServer.Add(SerializableObject.GetRandomObject(), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + ////////////////////////////////// + // Owner Remove SerializableObject + var index = Random.Range(0, compObject.ListCollectionOwner.Value.Count - 1); + var valueIntRemove = compObject.ListCollectionOwner.Value[index]; + compObject.Remove(valueIntRemove, ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); + ////////////////////////////////// + // Server Remove SerializableObject + compObjectServer.Remove(valueIntRemove, ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compObject.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!"); + + //////////////////////////////////// + // Owner Change SerializableObject + 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 + compObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject(); + compObjectServer.ListCollectionServer.CheckDirtyState(); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + //////////////////////////////////// + // Owner Add Range SerializableObjects + compObject.AddRange(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add range failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); + ////////////////////////////////// + // Server Add Range SerializableObjects + compObjectServer.AddRange(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add range failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compObject.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!"); + + //////////////////////////////////// + // Owner Full Set SerializableObjects + compObject.FullSet(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); + ////////////////////////////////// + // Server Full Set SerializableObjects + compObjectServer.FullSet(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + //////////////////////////////////// + // Owner Clear + compObject.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!"); + ////////////////////////////////// + // Server Clear + compObjectServer.Clear(ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compObject.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!"); + + /////////////////////////////////////////////////////////////////////////// + // List> Nested List Validation + compListObject = client.LocalClient.PlayerObject.GetComponent(); + compListObjectServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListObject.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObject.name} component match! {compListObject.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}"); + + ////////////////////////////////// + // 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 + 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 + 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 + 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)); + AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}"); + + + yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListObject.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObject.name} component match! {compListObject.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compListObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}"); + + //////////////////////////////////// + // Owner Change List item + index = Random.Range(0, compListObject.ListCollectionOwner.Value.Count - 1); + compListObject.ListCollectionOwner.Value[index] = SerializableObject.GetListOfRandomObjects(5); + compListObject.ListCollectionOwner.CheckDirtyState(); + Assert.True(compListObject.ListCollectionOwner.IsDirty(), "Client Should be dirty!"); + yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} change index ({index}) failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}"); + + ////////////////////////////////// + // Server Change List item + index = Random.Range(0, compListObjectServer.ListCollectionServer.Value.Count - 1); + compListObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetListOfRandomObjects(5); + compListObjectServer.ListCollectionServer.CheckDirtyState(); + Assert.True(compListObjectServer.ListCollectionServer.IsDirty(), "Server Should be dirty!"); + yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}"); + + //////////////////////////////////// + // 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 + 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()}"); + + yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListObject.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compListObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match!"); + + //////////////////////////////////// + // 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> + 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> + 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> + compListObjectServer.Clear(ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}!"); + + yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compListObject.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObject.name} component match!"); + + yield return WaitForConditionOrTimeOut(() => compListObjectServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match!"); + } + } + } + + #region INETWORKSERIALIZABLE LIST TEST COMPONENT HELPERS + public class SerializableObject : INetworkSerializable, IEquatable + { + public static SerializableObject GetRandomObject() + { + var serializableObject = new SerializableObject() + { + FloatValue = Random.Range(float.MinValue, float.MaxValue), + IntValue = Random.Range(ushort.MinValue, ushort.MaxValue), + LongValue = Random.Range(int.MinValue, int.MaxValue), + }; + return serializableObject; + } + + public static List GetListOfRandomObjects(int count) + { + var list = new List(); + for (int i = 0; i < count; i++) + { + list.Add(GetRandomObject()); + } + return list; + } + + public static List> GetListOfListOfRandomObjects(int numberOfLists, int countPerList) + { + var list = new List>(); + for (int i = 0; i < numberOfLists; i++) + { + list.Add(GetListOfRandomObjects(countPerList)); + } + return list; + } + + + + public int IntValue; + public long LongValue; + public float FloatValue; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref IntValue); + serializer.SerializeValue(ref LongValue); + serializer.SerializeValue(ref FloatValue); + } + + public bool Equals(SerializableObject other) + { + return IntValue.Equals(other.IntValue) && LongValue.Equals(other.LongValue) && FloatValue.Equals(other.FloatValue); + } + + } + + public class ListTestHelperListSerializableObject : ListTestHelperBase, IListTestHelperBase> + { + public static Dictionary> Instances = new Dictionary>(); + + 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 + public Dictionary>>> NetworkVariableChanges = new Dictionary>>>(); + + public bool ValidateInstances() + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!CompareBothItems(ListCollectionOwner.Value, otherOwnerCollection.Value)) + { + return false; + } + if (!CompareBothItems(ListCollectionServer.Value, otherServerCollection.Value)) + { + return false; + } + } + return true; + } + + private bool CompareBothItems(List> first, List> second) + { + if (first.Count != second.Count) + { + LogMessage($"Local count ({first.Count}) did not match remote count ({second.Count})!"); + return false; + } + for (int i = 0; i < first.Count; i++) + { + if (!first[i].SequenceEqual(second[i])) + { + LogMessage($"Sequence set ({i}) does not match! Local[{i}].Count = {first[i].Count} Remote[{i}].Count = {second[i].Count}."); + if (first[i].Count == second[i].Count) + { + var subBuilder = new StringBuilder(); + for (int j = 0; j < first[i].Count; j++) + { + subBuilder.Append($"[{first[i][j]}][{second[i][j]}]"); + } + + LogMessage($"Compared: {subBuilder}"); + } + return false; + } + } + return true; + } + + private bool ChangesMatch(Dictionary>> local, Dictionary>> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (!CompareBothItems(local[deltaType], other[deltaType])) + { + LogMessage($"{deltaType}s did not match!"); + return false; + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + var trackChangesSuccess = true; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + trackChangesSuccess = false; + break; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + trackChangesSuccess = false; + break; + } + + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} did not match!"); + trackChangesSuccess = false; + break; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return trackChangesSuccess; + } + + private List> GetInitialValues() + { + var rootList = new List>(); + for (int i = 0; i < 10; i++) + { + rootList.Add(SerializableObject.GetListOfRandomObjects(5)); + } + return rootList; + } + + public NetworkVariable>> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public List> OnSetServerValues() + { + return GetInitialValues(); + } + + public List> OnSetOwnerValues() + { + return GetInitialValues(); + } + + + public void UpdateValue(List value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + var index = netVar.Value.IndexOf(value); + netVar.Value[index] = value; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Add(List value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value); + netVar.CheckDirtyState(); + } + + public void AddRange(List> values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.AddRange(values); + netVar.CheckDirtyState(); + } + + public void Insert(List value, int index, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + netVar.Value.Insert(index, value); + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Remove(List value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(value); + netVar.CheckDirtyState(); + } + + public void FullSet(List> values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, List> previous, List> current) + { + var contextTable = NetworkVariableChanges[target]; + var whatWasAdded = current.Except(previous).ToList(); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToList(); + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed].Clear(); + contextTable[DeltaTypes.UnChanged].Clear(); + for (int i = 0; i < current.Count; i++) + { + if (previous.Count > i && !current[i].SequenceEqual(previous[i])) + { + contextTable[DeltaTypes.Changed].Add(current[i]); + } + else if (!whatWasAdded.Contains(current[i]) && previous.Contains(current[i])) + { + contextTable[DeltaTypes.UnChanged].Add(current[i]); + } + } + + } + + public void OnServerListValuesChanged(List> previous, List> current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(List> previous, List> current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new List>()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new List>()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionServer.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + } + + public class ListTestHelperSerializableObject : ListTestHelperBase, IListTestHelperBase + { + public static Dictionary> Instances = new Dictionary>(); + + 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 + public Dictionary>> NetworkVariableChanges = new Dictionary>>(); + + public bool ValidateInstances() + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!ListCollectionOwner.Value.SequenceEqual(otherOwnerCollection.Value)) + { + return false; + } + if (!ListCollectionServer.Value.SequenceEqual(otherServerCollection.Value)) + { + return false; + } + } + return true; + } + + private bool ChangesMatch(Dictionary> local, Dictionary> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (local[deltaType].Count != other[deltaType].Count) + { + LogMessage($"{deltaType}s did not match!"); + return false; + } + + for (int i = 0; i < local[deltaType].Count; i++) + { + if (!local[deltaType][i].Equals(other[deltaType][i])) + { + LogMessage($"Sequence set ({i}) does not match! Local[{i}] = {local[deltaType][i]} Remote[{i}].Count = {other[deltaType][i]}."); + return false; + } + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} did not match!"); + return false; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return true; + } + + private List GetInitialValues() + { + return SerializableObject.GetListOfRandomObjects(10); + } + + public NetworkVariable> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public List OnSetServerValues() + { + return GetInitialValues(); + } + + public List OnSetOwnerValues() + { + return GetInitialValues(); + } + + + public void UpdateValue(SerializableObject value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + var index = netVar.Value.IndexOf(value); + netVar.Value[index] = value; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Add(SerializableObject value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value); + netVar.CheckDirtyState(); + } + + public void AddRange(List values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.AddRange(values); + netVar.CheckDirtyState(); + } + + public void Insert(SerializableObject value, int index, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + netVar.Value.Insert(index, value); + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Remove(SerializableObject value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(value); + netVar.CheckDirtyState(); + } + + public void FullSet(List values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, List previous, List current) + { + var contextTable = NetworkVariableChanges[target]; + + var whatWasAdded = current.Except(previous).ToList(); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToList(); + var whatWasNeitherAddedOrRemoved = current.Where((c) => previous.Contains(c) && !whatWasAdded.Contains(c)).ToList(); + var whatChanged = whatWasNeitherAddedOrRemoved.Where((c) => previous.Contains(c) && !previous.Where((d) => d.Equals(c)).FirstOrDefault().Equals(c)).ToList(); + var whatRemainedTheSame = whatWasNeitherAddedOrRemoved.Where((c) => !whatChanged.Contains(c)).ToList(); + + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed] = whatChanged; + contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame; + } + + public void OnServerListValuesChanged(List previous, List current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(List previous, List current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new List()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new List()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionOwner.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + } + + #endregion + + #region BUILT-IN LIST TEST COMPONENT HELPERS + public class ListTestHelperListInt : ListTestHelperBase, IListTestHelperBase> + { + public static Dictionary> Instances = new Dictionary>(); + + 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 + public Dictionary>>> NetworkVariableChanges = new Dictionary>>>(); + + public bool ValidateInstances() + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!CompareBothItems(ListCollectionOwner.Value, otherOwnerCollection.Value)) + { + return false; + } + if (!CompareBothItems(ListCollectionServer.Value, otherServerCollection.Value)) + { + return false; + } + } + return true; + } + + private bool CompareBothItems(List> first, List> second) + { + if (first.Count != second.Count) + { + LogMessage($"Local count ({first.Count}) did not match remote count ({second.Count})!"); + return false; + } + for (int i = 0; i < first.Count; i++) + { + if (!first[i].SequenceEqual(second[i])) + { + LogMessage($"Sequence set ({i}) does not match! Local[{i}].Count = {first[i].Count} Remote[{i}].Count = {second[i].Count}."); + if (first[i].Count == second[i].Count) + { + var subBuilder = new StringBuilder(); + for (int j = 0; j < first[i].Count; j++) + { + subBuilder.Append($"[{first[i][j]}][{second[i][j]}]"); + } + + LogMessage($"Compared: {subBuilder}"); + } + return false; + } + } + return true; + } + + private bool ChangesMatch(Dictionary>> local, Dictionary>> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (!CompareBothItems(local[deltaType], other[deltaType])) + { + LogMessage($"{deltaType}s did not match!"); + return false; + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + var trackChangesSuccess = true; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + trackChangesSuccess = false; + break; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + trackChangesSuccess = false; + break; + } + + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} did not match!"); + trackChangesSuccess = false; + break; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return trackChangesSuccess; + } + + private List> GetInitialValues() + { + var rootList = new List>(); + for (int i = 0; i < 10; i++) + { + var childList = new List(); + for (int j = 0; j < 10; j++) + { + childList.Add(Random.Range(short.MinValue, short.MaxValue)); + } + rootList.Add(childList); + } + return rootList; + } + + public NetworkVariable>> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public List> OnSetServerValues() + { + return GetInitialValues(); + } + + public List> OnSetOwnerValues() + { + return GetInitialValues(); + } + + + public void UpdateValue(List value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + var index = netVar.Value.IndexOf(value); + netVar.Value[index] = value; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Add(List value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value); + netVar.CheckDirtyState(); + } + + public void AddRange(List> values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.AddRange(values); + netVar.CheckDirtyState(); + } + + public void Insert(List value, int index, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + netVar.Value.Insert(index, value); + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Remove(List value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(value); + netVar.CheckDirtyState(); + } + + public void FullSet(List> values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, List> previous, List> current) + { + var contextTable = NetworkVariableChanges[target]; var whatWasAdded = current.Except(previous).ToList(); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToList(); + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed].Clear(); + contextTable[DeltaTypes.UnChanged].Clear(); + for (int i = 0; i < current.Count; i++) + { + if (previous.Count > i && !current[i].SequenceEqual(previous[i])) + { + contextTable[DeltaTypes.Changed].Add(current[i]); + } + else if (!whatWasAdded.Contains(current[i]) && previous.Contains(current[i])) + { + contextTable[DeltaTypes.UnChanged].Add(current[i]); + } + } + + } + + public void OnServerListValuesChanged(List> previous, List> current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(List> previous, List> current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new List>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new List>()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new List>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new List>()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionServer.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + + } + + public class ListTestHelperInt : ListTestHelperBase, IListTestHelperBase + { + public static Dictionary> Instances = new Dictionary>(); + + 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 + public Dictionary>> NetworkVariableChanges = new Dictionary>>(); + + public bool ValidateInstances() + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!ListCollectionOwner.Value.SequenceEqual(otherOwnerCollection.Value)) + { + return false; + } + if (!ListCollectionServer.Value.SequenceEqual(otherServerCollection.Value)) + { + return false; + } + } + return true; + } + + private bool ChangesMatch(Dictionary> local, Dictionary> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (local[deltaType].Count != other[deltaType].Count) + { + LogMessage($"{deltaType}s did not match!"); + return false; + } + + for (int i = 0; i < local[deltaType].Count; i++) + { + if (!local[deltaType][i].Equals(other[deltaType][i])) + { + LogMessage($"Sequence set ({i}) does not match! Local[{i}] = {local[deltaType][i]} Remote[{i}].Count = {other[deltaType][i]}."); + return false; + } + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} did not match!"); + return false; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return true; + } + + private List GetInitialValues() + { + var list = new List(); + for (int i = 0; i < 10; i++) + { + list.Add(Random.Range(0, ushort.MaxValue)); + } + return list; + } + + public NetworkVariable> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public List OnSetServerValues() + { + return GetInitialValues(); + } + + public List OnSetOwnerValues() + { + return GetInitialValues(); + } + + + public void UpdateValue(int value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + var index = netVar.Value.IndexOf(value); + netVar.Value[index] = value; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Add(int value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value); + netVar.CheckDirtyState(); + } + + public void AddRange(List values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.AddRange(values); + netVar.CheckDirtyState(); + } + + public void Insert(int value, int index, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + netVar.Value.Insert(index, value); + if (checkDirty) + { + netVar.CheckDirtyState(); + } + } + + public void Remove(int value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(value); + netVar.CheckDirtyState(); + } + + public void FullSet(List values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, List previous, List current) + { + var contextTable = NetworkVariableChanges[target]; + + var whatWasAdded = current.Except(previous).ToList(); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToList(); + var whatWasNeitherAddedOrRemoved = current.Where((c) => previous.Contains(c) && !whatWasAdded.Contains(c)).ToList(); + var whatChanged = whatWasNeitherAddedOrRemoved.Where((c) => previous.Contains(c) && !previous.Where((d) => d.Equals(c)).FirstOrDefault().Equals(c)).ToList(); + var whatRemainedTheSame = whatWasNeitherAddedOrRemoved.Where((c) => !whatChanged.Contains(c)).ToList(); + + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed] = whatChanged; + contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame; + } + + public void OnServerListValuesChanged(List previous, List current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(List previous, List current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new List()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new List()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new List()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new List()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionOwner.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + } + #endregion + + #region BASE TEST COMPONENT HELPERS + public class ListTestHelperBase : NetworkBehaviour + { + public enum Targets + { + Server, + Owner + } + + public enum DeltaTypes + { + Added, + Removed, + Changed, + UnChanged + } + + private StringBuilder m_StringBuilder = new StringBuilder(); + + public string GetLog() + { + return m_StringBuilder.ToString(); + } + + protected void LogMessage(string message) + { + m_StringBuilder.AppendLine(message); + } + + protected void LogStart() + { + m_StringBuilder.Clear(); + m_StringBuilder.AppendLine($"[Client-{NetworkManager.LocalClientId}][{name}] Log Started."); + } + + + public virtual bool CompareTrackedChanges(Targets target) + { + return false; + } + } + + public interface IListTestHelperBase + { + public bool ValidateInstances(); + + public NetworkVariable> GetNetVar(ListTestHelperBase.Targets target); + + public List OnSetServerValues(); + + public List OnSetOwnerValues(); + + public void UpdateValue(T value, ListTestHelperBase.Targets target, bool checkDirty = true); + + public void Add(T value, ListTestHelperBase.Targets target); + + public void AddRange(List values, ListTestHelperBase.Targets target); + + public void Insert(T value, int index, ListTestHelperBase.Targets target, bool checkDirty = true); + + public void Remove(T value, ListTestHelperBase.Targets target); + + public void FullSet(List values, ListTestHelperBase.Targets target); + + public void Clear(ListTestHelperBase.Targets target); + + public void TrackChanges(ListTestHelperBase.Targets target, List previous, List current); + + public void OnServerListValuesChanged(List previous, List current); + + public void OnOwnerListValuesChanged(List previous, List current); + + public void ResetTrackedChanges(); + } + #endregion +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs.meta new file mode 100644 index 0000000000..8a0dfc0af6 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 939ac41f36685f84e94a4b66ebbb6d8c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From eb0e0d829c550a200637f51666330bcc712ebf43 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 12 Aug 2024 17:10:07 -0500 Subject: [PATCH 09/12] test Added Dictionary and HashSet to the tests. --- .../NetworkVariableCollectionsTests.cs | 1207 +++++++++++++++++ 1 file changed, 1207 insertions(+) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs index da80cdde41..199fd42800 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs @@ -10,6 +10,15 @@ namespace Unity.Netcode.RuntimeTests { + /// + /// Validates using managed collections with NetworkVariable. + /// Managed Collections Tested: + /// - List + /// - Dictionary + /// - 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)] public class NetworkVariableCollectionsTests : NetcodeIntegrationTest @@ -35,6 +44,9 @@ protected override IEnumerator OnSetup() ListTestHelperListInt.ResetState(); ListTestHelperSerializableObject.ResetState(); ListTestHelperListSerializableObject.ResetState(); + DictionaryTestHelper.ResetState(); + NestedDictionaryTestHelper.ResetState(); + HashSetBaseTypeTestHelper.ResetState(); return base.OnSetup(); } @@ -44,6 +56,9 @@ protected override void OnCreatePlayerPrefab() m_PlayerPrefab.AddComponent(); m_PlayerPrefab.AddComponent(); m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); base.OnCreatePlayerPrefab(); } @@ -516,7 +531,1145 @@ public IEnumerator TestListSerializableObjectCollections() AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match!"); } } + + private int m_CurrentKey; + private int GetNextKey() + { + m_CurrentKey++; + return m_CurrentKey; + } + + [UnityTest] + public IEnumerator TestDictionaryCollections() + { + var compDictionary = (DictionaryTestHelper)null; + var compDictionaryServer = (DictionaryTestHelper)null; + var className = $"{nameof(DictionaryTestHelper)}"; + + var clientList = m_ClientNetworkManagers.ToList(); + if (m_ServerNetworkManager.IsHost) + { + clientList.Insert(0, m_ServerNetworkManager); + } + + m_CurrentKey = 1000; + + foreach (var client in clientList) + { + /////////////////////////////////////////////////////////////////////////// + // Dictionary> nested dictionaries + compDictionary = client.LocalClient.PlayerObject.GetComponent(); + 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); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + ////////////////////////////////// + // Server Add SerializableObject Entry + compDictionaryServer.Add((GetNextKey(), SerializableObject.GetRandomObject()), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + ////////////////////////////////// + // Owner Remove SerializableObject Entry + var index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); + var valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; + 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]; + 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()}"); + + 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 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()}"); + ////////////////////////////////// + // 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()}"); + + //////////////////////////////////// + // 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()}"); + + //////////////////////////////////// + // Owner Clear + compDictionary.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + ////////////////////////////////// + // Server Clear + 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()}"); + + 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()}"); + } + } + + [UnityTest] + public IEnumerator TestDictionaryNestedCollections() + { + var compDictionary = (NestedDictionaryTestHelper)null; + var compDictionaryServer = (NestedDictionaryTestHelper)null; + var className = $"{nameof(NestedDictionaryTestHelper)}"; + + var clientList = m_ClientNetworkManagers.ToList(); + if (m_ServerNetworkManager.IsHost) + { + clientList.Insert(0, m_ServerNetworkManager); + } + + m_CurrentKey = 1000; + + foreach (var client in clientList) + { + /////////////////////////////////////////////////////////////////////////// + // Dictionary> nested dictionaries + compDictionary = client.LocalClient.PlayerObject.GetComponent(); + 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 Dictionary + compDictionary.Add((GetNextKey(), NestedDictionaryTestHelper.GetDictionaryValues()), 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()}"); + ////////////////////////////////// + // Server Add Dictionary + compDictionaryServer.Add((GetNextKey(), NestedDictionaryTestHelper.GetDictionaryValues()), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + ////////////////////////////////// + // Owner Remove Dictionary + var index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); + var valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; + 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 Dictionary + index = Random.Range(0, compDictionaryServer.ListCollectionOwner.Value.Keys.Count - 1); + valueInt = compDictionaryServer.ListCollectionOwner.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()}"); + + 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 Change Dictionary + index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1); + valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index]; + compDictionary.ListCollectionOwner.Value[valueInt] = NestedDictionaryTestHelper.GetDictionaryValues(); + 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 Dictionary + index = Random.Range(0, compDictionaryServer.ListCollectionOwner.Value.Keys.Count - 1); + valueInt = compDictionaryServer.ListCollectionOwner.Value.Keys.ToList()[index]; + compDictionaryServer.ListCollectionServer.Value[index] = NestedDictionaryTestHelper.GetDictionaryValues(); + compDictionaryServer.ListCollectionServer.CheckDirtyState(); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server change failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + + //////////////////////////////////// + // Owner Full Set Nested Dictionaries + compDictionary.FullSet(NestedDictionaryTestHelper.GetNestedDictionaryValues(), 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 Nested Dictionaries + compDictionaryServer.FullSet(NestedDictionaryTestHelper.GetNestedDictionaryValues(), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}"); + + //////////////////////////////////// + // Owner Clear + compDictionary.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}"); + ////////////////////////////////// + // Server Clear + 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()}"); + + 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()}"); + } + } + + [UnityTest] + public IEnumerator TestHashSetBuiltInTypeCollections() + { + var compHashSet = (HashSetBaseTypeTestHelper)null; + var compHashSetServer = (HashSetBaseTypeTestHelper)null; + var className = $"{nameof(HashSetBaseTypeTestHelper)}"; + + var clientList = m_ClientNetworkManagers.ToList(); + if (m_ServerNetworkManager.IsHost) + { + clientList.Insert(0, m_ServerNetworkManager); + } + + m_CurrentKey = 1000; + + foreach (var client in clientList) + { + /////////////////////////////////////////////////////////////////////////// + // HashSet Single dimension list + compHashSet = client.LocalClient.PlayerObject.GetComponent(); + compHashSetServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent(); + yield return WaitForConditionOrTimeOut(() => compHashSet.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compHashSet.OwnerClientId}'s {className} {compHashSet.name} component match! {compHashSet.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compHashSetServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compHashSetServer.OwnerClientId}'s {className} {compHashSetServer.name} component match! {compHashSetServer.GetLog()}"); + + ////////////////////////////////// + // Owner Add Item + compHashSet.Add(Random.Range(ushort.MinValue,ushort.MaxValue), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compHashSet.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {className} {compHashSet.name}! {compHashSet.GetLog()}"); + ////////////////////////////////// + // Server Add Item + compHashSetServer.Add(Random.Range(ushort.MinValue, ushort.MaxValue), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compHashSetServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server add failed to synchronize on {className} {compHashSetServer.name}! {compHashSetServer.GetLog()}"); + ////////////////////////////////// + // Owner Remove Item + var index = Random.Range(0, compHashSet.ListCollectionOwner.Value.Count - 1); + compHashSet.Remove(compHashSet.ListCollectionOwner.Value.ElementAt(index), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compHashSet.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {className} {compHashSet.name}! {compHashSet.GetLog()}"); + ////////////////////////////////// + // Server Remove Item + index = Random.Range(0, compHashSetServer.ListCollectionOwner.Value.Count - 1); + compHashSetServer.Remove(compHashSetServer.ListCollectionOwner.Value.ElementAt(index), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compHashSetServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server remove failed to synchronize on {className} {compHashSetServer.name}! {compHashSetServer.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compHashSet.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compHashSet.OwnerClientId}'s {className} {compHashSet.name} component match! {compHashSet.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compHashSetServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compHashSetServer.OwnerClientId}'s {className} {compHashSetServer.name} component match! {compHashSetServer.GetLog()}"); + + //////////////////////////////////// + // Owner Full Set HashSet Values + compHashSet.FullSet(HashSetBaseTypeTestHelper.GetHashSetValues(), ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compHashSet.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {className} {compHashSet.name}! {compHashSet.GetLog()}"); + ////////////////////////////////// + // Server Full Set HashSet Values + compHashSetServer.FullSet(HashSetBaseTypeTestHelper.GetHashSetValues(), ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compHashSetServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server full set failed to synchronize on {className} {compHashSetServer.name}! {compHashSetServer.GetLog()}"); + + //////////////////////////////////// + // Owner Clear + compHashSet.Clear(ListTestHelperBase.Targets.Owner); + yield return WaitForConditionOrTimeOut(() => compHashSet.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); + AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compHashSet.name}! {compHashSet.GetLog()}"); + ////////////////////////////////// + // Server Clear + compHashSetServer.Clear(ListTestHelperBase.Targets.Server); + yield return WaitForConditionOrTimeOut(() => compHashSetServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); + AssertOnTimeout($"Server clear failed to synchronize on {className} {compHashSetServer.name}! {compHashSetServer.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compHashSet.ValidateInstances()); + AssertOnTimeout($"[Owner] Not all instances of client-{compHashSet.OwnerClientId}'s {className} {compHashSet.name} component match! {compHashSet.GetLog()}"); + + yield return WaitForConditionOrTimeOut(() => compHashSetServer.ValidateInstances()); + AssertOnTimeout($"[Server] Not all instances of client-{compHashSetServer.OwnerClientId}'s {className} {compHashSetServer.name} component match! {compHashSetServer.GetLog()}"); + } + + } + } + + #region HASHSET COMPONENT HELPERS + public class HashSetBaseTypeTestHelper : ListTestHelperBase, IHashSetTestHelperBase + { + public static Dictionary> Instances = new Dictionary>(); + + public static void ResetState() + { + Instances.Clear(); + } + + public NetworkVariable> ListCollectionServer = new NetworkVariable>(new HashSet(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + public NetworkVariable> ListCollectionOwner = new NetworkVariable>(new HashSet(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + // This tracks what has changed per instance which is used to compare to all other instances + public Dictionary>> NetworkVariableChanges = new Dictionary>>(); + + public bool ValidateInstances() + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!ListCollectionOwner.Value.SequenceEqual(otherOwnerCollection.Value)) + { + return false; + } + if (!ListCollectionServer.Value.SequenceEqual(otherServerCollection.Value)) + { + return false; + } + } + return true; + } + + private bool ChangesMatch(Dictionary> local, Dictionary> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (local[deltaType].Count != other[deltaType].Count) + { + LogMessage($"{deltaType}s did not match!"); + return false; + } + foreach(var value in local[deltaType]) + { + if (!other[deltaType].Contains(value)) + { + LogMessage($"Value ({value}) in local was not found on remote!"); + return false; + } + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} did not match!"); + return false; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return true; + } + + public static HashSet GetHashSetValues(int count = 5) + { + var hashSet = new HashSet(); + for (int i = 0; i < count; i++) + { + hashSet.Add(Random.Range(ushort.MinValue, ushort.MaxValue)); + } + return hashSet; + } + + public NetworkVariable> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public HashSet OnSetServerValues() + { + return GetHashSetValues(); + } + + public HashSet OnSetOwnerValues() + { + return GetHashSetValues(); + } + + public void Add(int value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value); + netVar.CheckDirtyState(); + } + + public void AddRange(HashSet values, Targets target) + { + var netVar = GetNetVar(target); + foreach(var value in values) + { + netVar.Value.Add(value); + } + netVar.CheckDirtyState(); + } + + public void Remove(int value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(value); + netVar.CheckDirtyState(); + } + + public void FullSet(HashSet values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, HashSet previous, HashSet current) + { + var contextTable = NetworkVariableChanges[target]; + + var whatWasAdded = current.Except(previous).ToHashSet(); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToHashSet(); + var whatWasNeitherAddedOrRemoved = current.Where((c) => previous.Contains(c) && !whatWasAdded.Contains(c)).ToHashSet(); + var whatChanged = whatWasNeitherAddedOrRemoved.Where((c) => previous.Contains(c) && !previous.Where((d) => d.Equals(c)).FirstOrDefault().Equals(c)).ToHashSet(); + var whatRemainedTheSame = whatWasNeitherAddedOrRemoved.Where((c) => !whatChanged.Contains(c)).ToHashSet(); + + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed] = whatChanged; + contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame; + } + + public void OnServerListValuesChanged(HashSet previous, HashSet current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(HashSet previous, HashSet current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new HashSet()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new HashSet()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new HashSet()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new HashSet()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new HashSet()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new HashSet()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new HashSet()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new HashSet()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionOwner.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + } + #endregion + + #region DICTIONARY COMPONENT HELPERS + public class NestedDictionaryTestHelper : ListTestHelperBase, IDictionaryTestHelperBase> + { + public static Dictionary> Instances = new Dictionary>(); + + public static void ResetState() + { + Instances.Clear(); + } + + public NetworkVariable>> ListCollectionServer = new NetworkVariable>>(new Dictionary>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + public NetworkVariable>> ListCollectionOwner = new NetworkVariable>>(new Dictionary>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + // This tracks what has changed per instance which is used to compare to all other instances + public Dictionary>>> NetworkVariableChanges = new Dictionary>>>(); + + private bool CompareDictionaries(ulong clientId, Dictionary first, Dictionary second) + { + foreach (var entry in first) + { + if (!second.ContainsKey(entry.Key)) + { + LogMessage($"Client-{clientId} has no key entry for ({entry.Key})!"); + return false; + } + var seconValue = second[entry.Key]; + if (!entry.Value.Equals(seconValue)) + { + LogMessage($"Client-{clientId} value ({seconValue} does not equal ({entry.Value})!"); + return false; + } + } + return true; + } + + private bool CompareNestedDictionaries(ulong clientId, Dictionary> first, Dictionary> second) + { + foreach (var entry in first) + { + if (!second.ContainsKey(entry.Key)) + { + LogMessage($"Client-{clientId} has no key entry for ({entry.Key})!"); + return false; + } + var secondValue = second[entry.Key]; + if (!CompareDictionaries(clientId, entry.Value, secondValue)) + { + LogMessage($"Client-{clientId} value root Key ({entry.Key}) dictionary does not equal the local dictionary!"); + return false; + } + } + return true; + } + + public bool ValidateInstances() + { + LogStart(); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + LogMessage($"Client-{clientId} has no entry!"); + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + LogMessage($"Client-{clientId} has no instance entry of NetworkObject ({NetworkObjectId})!"); + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + + if (!CompareNestedDictionaries(clientId, ListCollectionOwner.Value, otherOwnerCollection.Value)) + { + LogMessage($"Client-{clientId} did not synchronize properly with the owner collection!"); + return false; + } + + if (!CompareNestedDictionaries(clientId, ListCollectionServer.Value, otherServerCollection.Value)) + { + LogMessage($"Client-{clientId} did not synchronize properly with the server collection!"); + return false; + } + } + return true; + } + + private bool ChangesMatch(ulong clientId, Dictionary>> local, Dictionary>> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (local[deltaType].Count != other[deltaType].Count) + { + LogMessage($"{deltaType}s count did not match!"); + return false; + } + if (!CompareNestedDictionaries(clientId, local[deltaType], other[deltaType])) + { + LogMessage($"{deltaType}s values did not match!"); + return false; + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(clientId, localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} failed to synchronize properly!"); + return false; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return true; + } + + public static Dictionary GetDictionaryValues(int count = 5) + { + var dictionary = new Dictionary(); + for (int i = 0; i < count; i++) + { + dictionary.Add(i, SerializableObject.GetRandomObject()); + } + return dictionary; + } + + public static Dictionary> GetNestedDictionaryValues(int count = 5) + { + var dictionary = new Dictionary>(); + for (int i = 0; i < count; i++) + { + dictionary.Add(i, GetDictionaryValues()); + } + return dictionary; + } + + public NetworkVariable>> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public Dictionary> OnSetServerValues() + { + + return GetNestedDictionaryValues(); + } + + public Dictionary> OnSetOwnerValues() + { + return GetNestedDictionaryValues(); + } + + + public bool UpdateValue((int, Dictionary) value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + if (netVar.Value.ContainsKey(value.Item1)) + { + netVar.Value[value.Item1] = value.Item2; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + return true; + } + return false; + } + + public void Add((int, Dictionary) value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value.Item1, value.Item2); + netVar.CheckDirtyState(); + } + + public void Remove(int key, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(key); + netVar.CheckDirtyState(); + } + + public void FullSet(Dictionary> values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, Dictionary> previous, Dictionary> current) + { + var contextTable = NetworkVariableChanges[target]; + + var whatWasAdded = current.Except(previous).ToDictionary(item => item.Key, item => item.Value); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + var whatWasNeitherAddedOrRemoved = current.Where((c) => previous.Contains(c) && !whatWasAdded.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + var whatChanged = whatWasNeitherAddedOrRemoved.Where((c) => previous.Contains(c) && !previous.Where((d) => d.Equals(c)).FirstOrDefault().Equals(c)).ToDictionary(item => item.Key, item => item.Value); + var whatRemainedTheSame = whatWasNeitherAddedOrRemoved.Where((c) => !whatChanged.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed] = whatChanged; + contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame; + } + + public void OnServerListValuesChanged(Dictionary> previous, Dictionary> current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(Dictionary> previous, Dictionary> current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new Dictionary>()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new Dictionary>()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionOwner.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } + } + + public class DictionaryTestHelper : ListTestHelperBase, IDictionaryTestHelperBase + { + public static Dictionary> Instances = new Dictionary>(); + + public static void ResetState() + { + Instances.Clear(); + } + + public NetworkVariable> ListCollectionServer = new NetworkVariable>(new Dictionary(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + public NetworkVariable> ListCollectionOwner = new NetworkVariable>(new Dictionary(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + // This tracks what has changed per instance which is used to compare to all other instances + public Dictionary>> NetworkVariableChanges = new Dictionary>>(); + + private bool CompareDictionaries(ulong clientId, Dictionary first, Dictionary second) + { + foreach (var entry in first) + { + if (!second.ContainsKey(entry.Key)) + { + LogMessage($"Client-{clientId} has no key entry for ({entry.Key})!"); + return false; + } + var seconValue = second[entry.Key]; + if (!entry.Value.Equals(seconValue)) + { + LogMessage($"Client-{clientId} value ({seconValue} does not equal ({entry.Value})!"); + return false; + } + } + return true; + } + + public bool ValidateInstances() + { + LogStart(); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + LogMessage($"Client-{clientId} has no entry!"); + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + LogMessage($"Client-{clientId} has no instance entry of NetworkObject ({NetworkObjectId})!"); + return false; + } + var otherOwnerCollection = Instances[clientId][NetworkObjectId].ListCollectionOwner; + var otherServerCollection = Instances[clientId][NetworkObjectId].ListCollectionServer; + if (!CompareDictionaries(clientId, ListCollectionOwner.Value, otherOwnerCollection.Value)) + { + LogMessage($"Client-{clientId} did not synchronize properly with the owner collection!"); + return false; + } + + if (!CompareDictionaries(clientId, ListCollectionServer.Value, otherServerCollection.Value)) + { + LogMessage($"Client-{clientId} did not synchronize properly with the server collection!"); + return false; + } + } + return true; + } + + private bool ChangesMatch(ulong clientId, Dictionary> local, Dictionary> other) + { + var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType().ToList(); + foreach (var deltaType in deltaTypes) + { + LogMessage($"Comparing {deltaType}:"); + if (local[deltaType].Count != other[deltaType].Count) + { + LogMessage($"{deltaType}s count did not match!"); + return false; + } + if (!CompareDictionaries(clientId, local[deltaType], other[deltaType])) + { + LogMessage($"{deltaType}s values did not match!"); + return false; + } + } + return true; + } + + public override bool CompareTrackedChanges(Targets target) + { + LogStart(); + var localChanges = NetworkVariableChanges[target]; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId) + { + continue; + } + if (!Instances.ContainsKey(clientId)) + { + return false; + } + if (!Instances[clientId].ContainsKey(NetworkObjectId)) + { + return false; + } + var entry = Instances[clientId][NetworkObjectId]; + var otherChanges = entry.NetworkVariableChanges[target]; + LogMessage($"Comparing against client-{clientId} {entry.name}:"); + if (!ChangesMatch(clientId, localChanges, otherChanges)) + { + LogMessage($"Client-{clientId} {entry.name} failed to synchronize properly!"); + return false; + } + LogMessage($"Client-{clientId} {entry.name} matched!"); + } + return true; + } + + public static Dictionary GetDictionaryValues(int count = 5) + { + var dictionary = new Dictionary(); + for (int i = 0; i < count; i++) + { + dictionary.Add(i, SerializableObject.GetRandomObject()); + } + return dictionary; + } + + public NetworkVariable> GetNetVar(Targets target) + { + return target == Targets.Server ? ListCollectionServer : ListCollectionOwner; + } + + public Dictionary OnSetServerValues() + { + return GetDictionaryValues(); + } + + public Dictionary OnSetOwnerValues() + { + return GetDictionaryValues(); + } + + + public bool UpdateValue((int, SerializableObject) value, Targets target, bool checkDirty = true) + { + var netVar = GetNetVar(target); + if (netVar.Value.ContainsKey(value.Item1)) + { + netVar.Value[value.Item1] = value.Item2; + if (checkDirty) + { + netVar.CheckDirtyState(); + } + return true; + } + return false; + } + + public void Add((int, SerializableObject) value, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Add(value.Item1, value.Item2); + netVar.CheckDirtyState(); + } + + public void Remove(int key, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Remove(key); + netVar.CheckDirtyState(); + } + + public void FullSet(Dictionary values, Targets target) + { + var netVar = GetNetVar(target); + netVar.Value = values; + netVar.CheckDirtyState(); + } + + public void Clear(Targets target) + { + var netVar = GetNetVar(target); + netVar.Value.Clear(); + netVar.CheckDirtyState(); + } + + public void TrackChanges(Targets target, Dictionary previous, Dictionary current) + { + var contextTable = NetworkVariableChanges[target]; + + var whatWasAdded = current.Except(previous).ToDictionary(item => item.Key, item => item.Value); + var whatWasRemoved = previous.Where((c) => !current.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + var whatWasNeitherAddedOrRemoved = current.Where((c) => previous.Contains(c) && !whatWasAdded.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + var whatChanged = whatWasNeitherAddedOrRemoved.Where((c) => previous.Contains(c) && !previous.Where((d) => d.Equals(c)).FirstOrDefault().Equals(c)).ToDictionary(item => item.Key, item => item.Value); + var whatRemainedTheSame = whatWasNeitherAddedOrRemoved.Where((c) => !whatChanged.Contains(c)).ToDictionary(item => item.Key, item => item.Value); + + contextTable[DeltaTypes.Added] = whatWasAdded; + contextTable[DeltaTypes.Removed] = whatWasRemoved; + contextTable[DeltaTypes.Changed] = whatChanged; + contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame; + } + + public void OnServerListValuesChanged(Dictionary previous, Dictionary current) + { + TrackChanges(Targets.Server, previous, current); + } + + public void OnOwnerListValuesChanged(Dictionary previous, Dictionary current) + { + TrackChanges(Targets.Owner, previous, current); + } + + /// + /// Keeps track of each client instsnce releative player instance with this component + /// + private void TrackRelativeInstances() + { + if (!Instances.ContainsKey(NetworkManager.LocalClientId)) + { + Instances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + + if (!Instances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + Instances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + ResetTrackedChanges(); + } + + public void ResetTrackedChanges() + { + NetworkVariableChanges.Clear(); + NetworkVariableChanges.Add(Targets.Owner, new Dictionary>()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Added, new Dictionary()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Changed, new Dictionary()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.Removed, new Dictionary()); + NetworkVariableChanges[Targets.Owner].Add(DeltaTypes.UnChanged, new Dictionary()); + NetworkVariableChanges.Add(Targets.Server, new Dictionary>()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Added, new Dictionary()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Changed, new Dictionary()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.Removed, new Dictionary()); + NetworkVariableChanges[Targets.Server].Add(DeltaTypes.UnChanged, new Dictionary()); + } + + protected override void OnNetworkPostSpawn() + { + TrackRelativeInstances(); + + ListCollectionServer.OnValueChanged += OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged; + + if (IsServer) + { + ListCollectionServer.Value = OnSetServerValues(); + ListCollectionOwner.CheckDirtyState(); + } + + if (IsOwner) + { + ListCollectionOwner.Value = OnSetOwnerValues(); + ListCollectionOwner.CheckDirtyState(); + } + base.OnNetworkPostSpawn(); + } + public override void OnNetworkDespawn() + { + ListCollectionServer.OnValueChanged -= OnServerListValuesChanged; + ListCollectionOwner.OnValueChanged -= OnOwnerListValuesChanged; + base.OnNetworkDespawn(); + } } + #endregion #region INETWORKSERIALIZABLE LIST TEST COMPONENT HELPERS public class SerializableObject : INetworkSerializable, IEquatable @@ -1783,5 +2936,59 @@ public interface IListTestHelperBase public void ResetTrackedChanges(); } + + public interface IDictionaryTestHelperBase + { + public bool ValidateInstances(); + + public NetworkVariable> GetNetVar(ListTestHelperBase.Targets target); + + public Dictionary OnSetServerValues(); + + public Dictionary OnSetOwnerValues(); + + public bool UpdateValue((TKey, TValue) value, ListTestHelperBase.Targets target, bool checkDirty = true); + + public void Add((TKey, TValue) value, ListTestHelperBase.Targets target); + + public void Remove(TKey key, ListTestHelperBase.Targets target); + + public void FullSet(Dictionary values, ListTestHelperBase.Targets target); + + public void Clear(ListTestHelperBase.Targets target); + + public void TrackChanges(ListTestHelperBase.Targets target, Dictionary previous, Dictionary current); + + public void OnServerListValuesChanged(Dictionary previous, Dictionary current); + + public void OnOwnerListValuesChanged(Dictionary previous, Dictionary current); + + public void ResetTrackedChanges(); + } + + public interface IHashSetTestHelperBase + { + public bool ValidateInstances(); + + public NetworkVariable> GetNetVar(ListTestHelperBase.Targets target); + + public HashSet OnSetServerValues(); + + public HashSet OnSetOwnerValues(); + + public void Add(T value, ListTestHelperBase.Targets target); + + public void Remove(T value, ListTestHelperBase.Targets target); + + public void Clear(ListTestHelperBase.Targets target); + + public void TrackChanges(ListTestHelperBase.Targets target, HashSet previous, HashSet current); + + public void OnServerListValuesChanged(HashSet previous, HashSet current); + + public void OnOwnerListValuesChanged(HashSet previous, HashSet current); + + public void ResetTrackedChanges(); + } #endregion } From c268af4eeff72022a86ec0b8b9cff05d374154fe Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 12 Aug 2024 17:21:05 -0500 Subject: [PATCH 10/12] style Removing whitespaces --- .../Tests/Runtime/NetworkVariableCollectionsTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs index 199fd42800..7d7e785575 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs @@ -778,7 +778,7 @@ public IEnumerator TestHashSetBuiltInTypeCollections() ////////////////////////////////// // Owner Add Item - compHashSet.Add(Random.Range(ushort.MinValue,ushort.MaxValue), ListTestHelperBase.Targets.Owner); + compHashSet.Add(Random.Range(ushort.MinValue, ushort.MaxValue), ListTestHelperBase.Targets.Owner); yield return WaitForConditionOrTimeOut(() => compHashSet.CompareTrackedChanges(ListTestHelperBase.Targets.Owner)); AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {className} {compHashSet.name}! {compHashSet.GetLog()}"); ////////////////////////////////// @@ -795,7 +795,7 @@ public IEnumerator TestHashSetBuiltInTypeCollections() ////////////////////////////////// // Server Remove Item index = Random.Range(0, compHashSetServer.ListCollectionOwner.Value.Count - 1); - compHashSetServer.Remove(compHashSetServer.ListCollectionOwner.Value.ElementAt(index), ListTestHelperBase.Targets.Server); + compHashSetServer.Remove(compHashSetServer.ListCollectionOwner.Value.ElementAt(index), ListTestHelperBase.Targets.Server); yield return WaitForConditionOrTimeOut(() => compHashSetServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server)); AssertOnTimeout($"Server remove failed to synchronize on {className} {compHashSetServer.name}! {compHashSetServer.GetLog()}"); @@ -893,7 +893,7 @@ private bool ChangesMatch(Dictionary> local, Dictionary LogMessage($"{deltaType}s did not match!"); return false; } - foreach(var value in local[deltaType]) + foreach (var value in local[deltaType]) { if (!other[deltaType].Contains(value)) { @@ -971,10 +971,10 @@ public void Add(int value, Targets target) public void AddRange(HashSet values, Targets target) { var netVar = GetNetVar(target); - foreach(var value in values) + foreach (var value in values) { netVar.Value.Add(value); - } + } netVar.CheckDirtyState(); } @@ -1255,7 +1255,7 @@ public NetworkVariable>> Get public Dictionary> OnSetServerValues() { - + return GetNestedDictionaryValues(); } From 33abafe2eb8610abb1f67f1f2b0bf1890538574d Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 12 Aug 2024 17:25:29 -0500 Subject: [PATCH 11/12] update adding changelog entries --- com.unity.netcode.gameobjects/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 92609a319c..77e6c2416c 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -8,10 +8,19 @@ Additional documentation and release notes are available at [Multiplayer Documen [Unreleased] ### Added +- 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) + ### Fixed +- Fixed issue using collections within `NetworkVariable` where the collection would not detect changes to items or nested items. (#3005) +- Fixed issue where `List`, `Dictionary`, and `HashSet` collections would not uniquely duplicate nested collections. (#3005) + ### Changed +- Changed permissions exception thrown in `NetworkList` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3005) +- Changed permissions exception thrown in `NetworkVariable.Value` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3005) + + ## [1.10.0] - 2024-07-22 ### Added From 950f5add80e213463a91b31ed867a08775e2f6a2 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Tue, 13 Aug 2024 12:13:02 -0500 Subject: [PATCH 12/12] update Adding missed change log entries. --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 77e6c2416c..cca505e936 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -14,6 +14,8 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed issue using collections within `NetworkVariable` where the collection would not detect changes to items or nested items. (#3005) - Fixed issue where `List`, `Dictionary`, and `HashSet` collections would not uniquely duplicate nested collections. (#3005) +- 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) +- 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) ### Changed