From 511af606f73172ebf68df5ffc7b1c239be9f8b69 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 20 Jan 2026 18:41:20 -0600 Subject: [PATCH 1/7] fix Only the spawn authority should reset the sent time and reset the dirty flag for NetworkList changes to avoid sending both the CreateObjectMessage (which would contain the changes) plus the NetworkVariableDeltaMessage. --- .../Runtime/NetworkVariable/Collections/NetworkList.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index 42bc1b22d0..da191be04f 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -61,10 +61,12 @@ public NetworkList(IEnumerable values = default, internal override void OnSpawned() { // If we are dirty and have write permissions by the time the NetworkObject - // is finished spawning (same frame), then go ahead and reset the dirty related - // properties for NetworkList in the event user script has made changes when - // spawning to prevent duplicate entries. - if (IsDirty() && CanSend()) + // is finished spawning (same frame) and the instance is on the spawn authority + // side, then go ahead and reset the dirty related properties for NetworkList + // in the event user script has made changes when spawning to prevent duplicate + // entries (i.e. they are sent via CreateObjectMessage so we don't need to send + // the NetworkVariableDeltaMessage. + if (IsDirty() && CanSend() && m_NetworkBehaviour.HasAuthority) { UpdateLastSentTime(); ResetDirty(); From d09a7c9fbe9623d91c70b3c9146066b366acbf27 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 00:12:54 -0600 Subject: [PATCH 2/7] style updating comment in area updated for further clarity on the requirements to resetting the dirty and last sent time after the associated NetworkObject has been spawned. --- .../NetworkVariable/Collections/NetworkList.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index da191be04f..bf69446dbf 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -60,12 +60,17 @@ public NetworkList(IEnumerable values = default, internal override void OnSpawned() { - // If we are dirty and have write permissions by the time the NetworkObject - // is finished spawning (same frame) and the instance is on the spawn authority - // side, then go ahead and reset the dirty related properties for NetworkList - // in the event user script has made changes when spawning to prevent duplicate - // entries (i.e. they are sent via CreateObjectMessage so we don't need to send - // the NetworkVariableDeltaMessage. + // If the NetworkList is: + // - Dirty + // - State updates can be sent: + // -- The instance has write permissions. + // -- The last sent time plus the max send time period is less than the current time. + // - User script has modified the list during spawn. + // - This instance is on the spawn authority side. + // Then by the time the NetworkObject is finished spawning (on the same frame), then go + // ahead and reset the dirty related properties and last sent time to prevent duplicate + // entries from being sent (i.e. CreateObjectMessage will contain the changes so we + // don't need to send a proceeding NetworkVariableDeltaMessage). if (IsDirty() && CanSend() && m_NetworkBehaviour.HasAuthority) { UpdateLastSentTime(); From ccd87bc5d81187076906d0ee3e6d09a6fe3507d6 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 00:21:58 -0600 Subject: [PATCH 3/7] update Adding change log entry. --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 30247ba839..5bfa2d2c0f 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -24,8 +24,10 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue when using a client-server topology where a NetworkList with owner write permissions was resetting sent time and dirty flags on owning clients that were not the spawn authority. (#3850) - Fixed an integer overflow that occurred when configuring a large disconnect timeout with Unity Transport. (#3810) + ### Security From 709f6d063a24ce61bda5eca01c5ff28ddcbc4332 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 00:23:46 -0600 Subject: [PATCH 4/7] style Rephrasing change log entry. --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 5bfa2d2c0f..dd1c9347e2 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -24,7 +24,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed -- Fixed issue when using a client-server topology where a NetworkList with owner write permissions was resetting sent time and dirty flags on owning clients that were not the spawn authority. (#3850) +- Fixed issue when using a client-server topology where a `NetworkList` with owner write permissions was resetting sent time and dirty flags after having been spawned on owning clients that were not the spawn authority. (#3850) - Fixed an integer overflow that occurred when configuring a large disconnect timeout with Unity Transport. (#3810) From 2f6df55e3eeb83f5e7f5675480f0629de1c9e19c Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 00:27:16 -0600 Subject: [PATCH 5/7] style One last adjustment to the comment over the area fixed. --- .../Runtime/NetworkVariable/Collections/NetworkList.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index bf69446dbf..2b2410dc67 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -67,10 +67,10 @@ internal override void OnSpawned() // -- The last sent time plus the max send time period is less than the current time. // - User script has modified the list during spawn. // - This instance is on the spawn authority side. - // Then by the time the NetworkObject is finished spawning (on the same frame), then go - // ahead and reset the dirty related properties and last sent time to prevent duplicate - // entries from being sent (i.e. CreateObjectMessage will contain the changes so we - // don't need to send a proceeding NetworkVariableDeltaMessage). + // When the NetworkObject is finished spawning (on the same frame), go ahead and reset + // the dirty related properties and last sent time to prevent duplicate entries from + // being sent (i.e. CreateObjectMessage will contain the changes so we don't need to + // send a proceeding NetworkVariableDeltaMessage). if (IsDirty() && CanSend() && m_NetworkBehaviour.HasAuthority) { UpdateLastSentTime(); From 4d56f50472c67dadf54db5a793e8d9ede155157c Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 12:33:33 -0600 Subject: [PATCH 6/7] test Updating NetworkListTests to include an owner write test scenario that validates initial spawn, late join with spawn, and then changes to the owner write NetworkLists. --- .../NetworkVariable/NetworkListTests.cs | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs index 15c737fbc3..2e7e9b6532 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs @@ -27,6 +27,12 @@ public NetworkListTests(HostOrServer host) : base(host) { } private ulong m_TestObjectId; + protected override IEnumerator OnSetup() + { + IsOwnerWriteTest = false; + return base.OnSetup(); + } + protected override void OnServerAndClientsCreated() { m_ListObjectPrefab = CreateNetworkObjectPrefab("ListObject"); @@ -285,6 +291,136 @@ private int[] Shuffle(List list) // This will do a shuffle of the list return list.OrderBy(_ => rng.Next()).ToArray(); } + + private List m_SpawnedObjects = new List(); + internal const int SpawnCount = 10; + internal bool IsOwnerWriteTest; + internal NetworkManager LateJoinedClient; + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + if (IsOwnerWriteTest) + { + LateJoinedClient = networkManager; + } + else + { + LateJoinedClient = null; + } + base.OnNewClientCreated(networkManager); + } + + [UnityTest] + public IEnumerator OwnerWriteTests() + { + IsOwnerWriteTest = true; + var authorityBetworkManager = GetAuthorityNetworkManager(); + m_SpawnedObjects.Clear(); + m_ExpectedValues.Clear(); + // Set our initial expected values as 0 - 9 + for (int i = 0; i < SpawnCount; i++) + { + m_ExpectedValues.Add(i); + } + + // Each spawned instance will be owned by each NetworkManager instance in order + // to validate owner write NetworkLists. + foreach (var networkManager in m_NetworkManagers) + { + m_SpawnedObjects.Add(SpawnObject(m_ListObjectPrefab, networkManager).GetComponent()); + } + + // Verify all NetworkManager instances spawned the objects + yield return WaitForSpawnedOnAllOrTimeOut(m_SpawnedObjects); + AssertOnTimeout("Not all instances were spawned on all clients!"); + + // Verify all spawned object instances have the expected owner write NetworkList values + yield return WaitForConditionOrTimeOut(OnVerifyOwnerWriteData); + AssertOnTimeout("Detected invalid count or value on one of the spawned instances!"); + + // Late join a client + yield return CreateAndStartNewClient(); + + // Spawn an instance with the new client being the owner + m_SpawnedObjects.Add(SpawnObject(m_ListObjectPrefab, LateJoinedClient).GetComponent()); + + // Verify all NetworkManager instances spawned the objects + yield return WaitForSpawnedOnAllOrTimeOut(m_SpawnedObjects); + AssertOnTimeout("Not all instances were spawned on all clients!"); + + // Verify all spawned object instances have the expected owner write NetworkList values + yield return WaitForConditionOrTimeOut(OnVerifyOwnerWriteData); + AssertOnTimeout("Detected invalid count or value on one of the spawned instances!"); + + // Now have all of the clients update their list values to randomly assigned values + // in order to verify changes to owner write NetworkLists are synchronized properly. + m_ExpectedValues.Clear(); + for (int i = 0; i < SpawnCount; i++) + { + m_ExpectedValues.Add(Random.Range(10, 100)); + } + UpdateOwnerWriteValues(); + + // Verify all spawned object instances have the expected owner write NetworkList values + yield return WaitForConditionOrTimeOut(OnVerifyOwnerWriteData); + AssertOnTimeout("Detected invalid count or value on one of the spawned instances!"); + } + + private void UpdateOwnerWriteValues() + { + foreach (var spawnedObject in m_SpawnedObjects) + { + var owningNetworkManager = m_NetworkManagers.Where((c) => c.LocalClientId == spawnedObject.OwnerClientId).First(); + var networkObjectId = spawnedObject.NetworkObjectId; + var listComponent = owningNetworkManager.SpawnManager.SpawnedObjects[networkObjectId].GetComponent(); + for (int i = 0; i < SpawnCount; i++) + { + listComponent.OwnerWriteList[i] = m_ExpectedValues[i]; + } + } + } + + private bool OnVerifyOwnerWriteData(StringBuilder errorLog) + { + foreach (var spawnedObject in m_SpawnedObjects) + { + var networkObjectId = spawnedObject.NetworkObjectId; + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] Does not have an instance of spawned object NetworkObjectId: {networkObjectId}"); + return false; + } + var listComponent = networkManager.SpawnManager.SpawnedObjects[networkObjectId].GetComponent(); + + if (listComponent == null) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] List component was not found"); + return false; + } + + if (listComponent.OwnerWriteList.Count != SpawnCount) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] List component has the incorrect number of items. Expected: {SpawnCount}, Have: {listComponent.TheList.Count}"); + return false; + } + + for (int i = 0; i < SpawnCount; i++) + { + var actual = listComponent.OwnerWriteList[i]; + var expected = m_ExpectedValues[i]; + if (expected != actual) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] Incorrect value at index {i}, expected: {expected}, actual: {actual}"); + return false; + } + } + } + } + + return true; + } } internal class NetworkListTest : NetworkBehaviour @@ -292,6 +428,7 @@ internal class NetworkListTest : NetworkBehaviour public readonly NetworkList TheList = new(); public readonly NetworkList TheStructList = new(); public readonly NetworkList TheLargeList = new(); + public readonly NetworkList OwnerWriteList = new NetworkList(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); private void ListChanged(NetworkListEvent e) { @@ -309,6 +446,18 @@ public override void OnDestroy() base.OnDestroy(); } + public override void OnNetworkSpawn() + { + if (IsOwner) + { + for (int i = 0; i < NetworkListTests.SpawnCount; i++) + { + OwnerWriteList.Add(i); + } + } + base.OnNetworkSpawn(); + } + public bool ListDelegateTriggered; } From 412639a113fbdaf065d2041ed31b5378a5753018 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 21 Jan 2026 12:40:30 -0600 Subject: [PATCH 7/7] style - PVP Removing trailing space from comment. --- .../Tests/Runtime/NetworkVariable/NetworkListTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs index 2e7e9b6532..9793d64f53 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkListTests.cs @@ -338,7 +338,7 @@ public IEnumerator OwnerWriteTests() yield return WaitForConditionOrTimeOut(OnVerifyOwnerWriteData); AssertOnTimeout("Detected invalid count or value on one of the spawned instances!"); - // Late join a client + // Late join a client yield return CreateAndStartNewClient(); // Spawn an instance with the new client being the owner