diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 92609a319..cca505e93 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -8,10 +8,21 @@ 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) +- 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 +- 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 diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index a082b4e92..a489e9dbf 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 @@ namespace Unity.Netcode // 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 @@ namespace Unity.Netcode // 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 @@ namespace Unity.Netcode // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + LogWritePermissionError(); + return false; } int index = m_List.IndexOf(item); @@ -451,7 +454,8 @@ namespace Unity.Netcode // 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 @@ namespace Unity.Netcode // 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 @@ namespace Unity.Netcode // 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 a17b70333..938d53401 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs @@ -79,27 +79,59 @@ namespace Unity.Netcode /// /// 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; 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)) { - throw new InvalidOperationException("Client is not allowed to write to this NetworkVariable"); + T previousValue = m_InternalValue; + m_InternalValue = value; + SetDirty(true); + m_IsDisposed = false; + OnValueChanged?.Invoke(previousValue, m_InternalValue); } - - Set(value); - m_IsDisposed = false; } } + /// + /// 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.
+ /// 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) + { + 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() { return ref m_InternalValue; @@ -184,9 +216,8 @@ namespace Unity.Netcode 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 +236,22 @@ namespace Unity.Netcode /// Whether or not the container should keep the dirty delta, or mark the delta as consumed public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { + // In order to get managed collections to properly have a previous and current value, we have to + // duplicate the collection at this point before making any modifications to the current. + m_HasPreviousValue = true; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + 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 22028dd6b..33f5fffb6 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -32,21 +32,47 @@ namespace Unity.Netcode /// 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() + { + return $"|Client-{m_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(); diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs index cda34a57e..70c94cf3b 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -349,7 +349,10 @@ namespace Unity.Netcode 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 @@ namespace Unity.Netcode 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 @@ namespace Unity.Netcode duplicatedValue.Clear(); foreach (var item in value) { - duplicatedValue.Add(item.Key, item.Value); + // Handles nested dictionaries + TKey subKey = default; + TVal subValue = default; + NetworkVariableSerialization.Duplicate(item.Key, ref subKey); + NetworkVariableSerialization.Duplicate(item.Value, ref subValue); + duplicatedValue.Add(subKey, subValue); } } } 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 000000000..7d7e78557 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableCollectionsTests.cs @@ -0,0 +1,2994 @@ +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 +{ + /// + /// 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 + { + 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(); + DictionaryTestHelper.ResetState(); + NestedDictionaryTestHelper.ResetState(); + HashSetBaseTypeTestHelper.ResetState(); + return base.OnSetup(); + } + + protected override void OnCreatePlayerPrefab() + { + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + 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!"); + } + } + + 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 + { + 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(); + } + + 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 +} 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 000000000..8a0dfc0af --- /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: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs index b3cef36fa..2029fa1e0 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs @@ -254,7 +254,8 @@ namespace Unity.Netcode.RuntimeTests 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 @@ namespace Unity.Netcode.RuntimeTests 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 @@ namespace Unity.Netcode.RuntimeTests { 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 7bd281644..0d8f36e0b 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; @@ -130,72 +129,44 @@ namespace Unity.Netcode.RuntimeTests 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);