Skip to content

Commit c754a2c

Browse files
fix: owner changing ownership causing identical previous and current ids when using a distributed authority network topology (#3347)
This fixes an issue with the synchronization of NetworkVariables when changing ownership where it was possible to have the previous owner be equal to the current owner when using a distributed authority network topology and the owner handled changing the ownership. This includes an additional check for ditry states of any collections base NetworkVariables. <!-- Add short version of the JIRA ticket to the PR title (e.g. "feat: new shiny feature [MTT-123]") --> fix: #3343 close: #3343 ## Changelog - Fixed issue where an owner that changes ownership, when using a distributed authority network topology, could yield identical previous and current owner identifiers. This could also cause `NetworkTransform` to fail to change ownership which would leave the previous owner still subscribed to network tick events. ## Testing and Documentation - Includes integration test `NetworkObjectOwnershipTests.TestAuthorityChangingOwnership`. - No documentation changes or additions were necessary. <!-- Uncomment and mark items off with a * if this PR deprecates any API: ### Deprecated API - [ ] An `[Obsolete]` attribute was added along with a `(RemovedAfter yyyy-mm-dd)` entry. - [ ] An [api updater] was added. - [ ] Deprecation of the API is explained in the CHANGELOG. - [ ] The users can understand why this API was removed and what they should use instead. -->
1 parent d80340d commit c754a2c

File tree

9 files changed

+168
-16
lines changed

9 files changed

+168
-16
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2020

2121
- Fixed issue when using a distributed authority network topology and many clients attempt to connect simultaneously the session owner could max-out the maximum in-flight reliable messages allowed, start dropping packets, and some of the connecting clients would fail to fully synchronize. (#3350)
2222
- Fixed issue when using a distributed authority network topology and scene management was disabled clients would not be able to spawn any new network prefab instances until synchronization was complete. (#3350)
23+
- Fixed issue where an owner that changes ownership, when using a distributed authority network topology, could yield identical previous and current owner identifiers. This could also cause `NetworkTransform` to fail to change ownership which would leave the previous owner still subscribed to network tick events. (#3347)
2324
- Fixed issue where the `MaximumInterpolationTime` could not be modified from within the inspector view or runtime. (#3337)
2425
- Fixed `ChangeOwnership` changing ownership to clients that are not observers. This also happened with automated object distribution. (#3323)
2526
- Fixed issue where `AnticipatedNetworkVariable` previous value returned by `AnticipatedNetworkVariable.OnAuthoritativeValueChanged` is updated correctly on the non-authoritative side. (#3306)

com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1185,14 +1185,23 @@ internal void MarkVariablesDirty(bool dirty)
11851185
}
11861186
}
11871187

1188-
internal void MarkOwnerReadVariablesDirty()
1188+
/// <summary>
1189+
/// For owner read permissions, when changing ownership we need to do a full synchronization
1190+
/// of all NetworkVariables that are owner read permission based since the owner is the only
1191+
/// instance that knows what the most current values are.
1192+
/// </summary>
1193+
internal void MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty()
11891194
{
11901195
for (int j = 0; j < NetworkVariableFields.Count; j++)
11911196
{
11921197
if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner)
11931198
{
11941199
NetworkVariableFields[j].SetDirty(true);
11951200
}
1201+
if (NetworkVariableFields[j].WritePerm == NetworkVariableWritePermission.Owner)
1202+
{
1203+
NetworkVariableFields[j].OnCheckIsDirtyState();
1204+
}
11961205
}
11971206
}
11981207

com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs

-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,6 @@ internal void NetworkBehaviourUpdate(bool forceSend = false)
108108
foreach (var dirtyobj in m_DirtyNetworkObjects)
109109
{
110110
dirtyobj.PostNetworkVariableWrite(forceSend);
111-
// Once done processing, we set the previous owner id to the current owner id
112-
dirtyobj.PreviousOwnerId = dirtyobj.OwnerClientId;
113111
}
114112
m_DirtyNetworkObjects.Clear();
115113
}

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

+29-2
Original file line numberDiff line numberDiff line change
@@ -2567,12 +2567,39 @@ internal void MarkVariablesDirty(bool dirty)
25672567
}
25682568
}
25692569

2570-
internal void MarkOwnerReadVariablesDirty()
2570+
/// <summary>
2571+
/// Used when changing ownership, this will mark any owner read permission base NetworkVariables as dirty
2572+
/// and will check if any owner write permission NetworkVariables are dirty (primarily for collections) so
2573+
/// the new owner will get a full state update prior to changing ownership.
2574+
/// </summary>
2575+
/// <remarks>
2576+
/// We have to pass in the original owner and previous owner to "reset" back to the current state of this
2577+
/// NetworkObject in order to preserve the same ownership change flow. By the time this is invoked, the
2578+
/// new and previous owner ids have already been set.
2579+
/// </remarks>
2580+
/// <param name="originalOwnerId">the owner prior to beginning the change in ownership change.</param>
2581+
/// <param name="originalPreviousOwnerId">the previous owner prior to beginning the change in ownership change.</param>
2582+
internal void SynchronizeOwnerNetworkVariables(ulong originalOwnerId, ulong originalPreviousOwnerId)
25712583
{
2584+
var currentOwnerId = OwnerClientId;
2585+
OwnerClientId = originalOwnerId;
2586+
PreviousOwnerId = originalPreviousOwnerId;
25722587
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
25732588
{
2574-
ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty();
2589+
ChildNetworkBehaviours[i].MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty();
25752590
}
2591+
2592+
// Now set the new owner and previous owner identifiers back to their original new values
2593+
// before we run the NetworkBehaviourUpdate. For owner read only permissions this order of
2594+
// operations is **particularly important** as we need to first (above) mark things as dirty
2595+
// from the context of the original owner and then second (below) we need to send the messages
2596+
// which requires the new owner to be set for owner read permission NetworkVariables.
2597+
OwnerClientId = currentOwnerId;
2598+
PreviousOwnerId = originalOwnerId;
2599+
2600+
// Force send a state update for all owner read NetworkVariables and any currently dirty
2601+
// owner write NetworkVariables.
2602+
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
25762603
}
25772604

25782605
// NGO currently guarantees that the client will receive spawn data for all objects in one network tick.

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs

+2-5
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,8 @@ private void HandleOwnershipChange(ref NetworkContext context)
381381

382382
if (originalOwner == networkManager.LocalClientId && !networkManager.DistributedAuthorityMode)
383383
{
384-
// Mark any owner read variables as dirty
385-
networkObject.MarkOwnerReadVariablesDirty();
386-
// Immediately queue any pending deltas and order the message before the
387-
// change in ownership message.
388-
networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
384+
// Fully synchronize NetworkVariables with either read or write ownership permissions.
385+
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, networkObject.PreviousOwnerId);
389386
}
390387

391388
// Always invoke ownership change notifications

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs

+7
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ public bool CheckDirtyState(bool forceCheck = false)
166166
return isDirty;
167167
}
168168

169+
/// <inheritdoc/>
170+
internal override void OnCheckIsDirtyState()
171+
{
172+
CheckDirtyState();
173+
base.OnCheckIsDirtyState();
174+
}
175+
169176
internal ref T RefValue()
170177
{
171178
return ref m_InternalValue;

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs

+9
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,15 @@ internal ulong OwnerClientId()
373373
return m_NetworkBehaviour.NetworkObject.OwnerClientId;
374374
}
375375

376+
/// <summary>
377+
/// Primarily to check for collections dirty states when doing
378+
/// a fully owner read/write NetworkVariable update.
379+
/// </summary>
380+
internal virtual void OnCheckIsDirtyState()
381+
{
382+
383+
}
384+
376385
/// <summary>
377386
/// Writes the dirty changes, that is, the changes since the variable was last dirty, to the writer
378387
/// </summary>

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,15 @@ internal void RemoveOwnership(NetworkObject networkObject)
432432

433433
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false)
434434
{
435+
if (clientId == networkObject.OwnerClientId)
436+
{
437+
if (NetworkManager.LogLevel <= LogLevel.Developer)
438+
{
439+
Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)");
440+
}
441+
return;
442+
}
443+
435444
// For client-server:
436445
// If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes,
437446
// then notify the user they could potentially lose state updates if developer logging is enabled.
@@ -530,6 +539,10 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool
530539
return;
531540
}
532541

542+
// Save the previous owner to work around a bit of a nasty issue with assuring NetworkVariables are serialized when ownership changes
543+
var originalPreviousOwnerId = networkObject.PreviousOwnerId;
544+
var originalOwner = networkObject.OwnerClientId;
545+
533546
// Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates
534547
networkObject.PreviousOwnerId = networkObject.OwnerClientId;
535548

@@ -545,13 +558,10 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool
545558
// Always notify locally on the server when a new owner is assigned
546559
networkObject.InvokeBehaviourOnGainedOwnership();
547560

548-
if (networkObject.PreviousOwnerId == NetworkManager.LocalClientId)
561+
// If we are the original owner, then we want to synchronize owner read & write NetworkVariables.
562+
if (originalOwner == NetworkManager.LocalClientId)
549563
{
550-
// Mark any owner read variables as dirty
551-
networkObject.MarkOwnerReadVariablesDirty();
552-
// Immediately queue any pending deltas and order the message before the
553-
// change in ownership message.
554-
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
564+
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId);
555565
}
556566

557567
var size = 0;

com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs

+94
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using NUnit.Framework;
5+
using Unity.Netcode.Components;
56
using Unity.Netcode.TestHelpers.Runtime;
67
using UnityEngine;
78
using UnityEngine.TestTools;
@@ -25,6 +26,12 @@ public override void OnGainedOwnership()
2526
OnGainedOwnershipFired = true;
2627
}
2728

29+
protected override void OnOwnershipChanged(ulong previous, ulong current)
30+
{
31+
Assert.True(previous != current, $"[{nameof(OnOwnershipChanged)}][Invalid Parameters] Invoked and the previous ({previous}) equals the current ({current})!");
32+
base.OnOwnershipChanged(previous, current);
33+
}
34+
2835
public override void OnNetworkSpawn()
2936
{
3037
if (!SpawnedInstances.ContainsKey(NetworkManager.LocalClientId))
@@ -39,6 +46,12 @@ public void ResetFlags()
3946
OnLostOwnershipFired = false;
4047
OnGainedOwnershipFired = false;
4148
}
49+
50+
[Rpc(SendTo.Authority)]
51+
public void ChangeOwnershipRpc(RpcParams rpcParams = default)
52+
{
53+
NetworkObject.ChangeOwnership(rpcParams.Receive.SenderClientId);
54+
}
4255
}
4356

4457

@@ -71,6 +84,7 @@ protected override void OnServerAndClientsCreated()
7184
{
7285
m_OwnershipPrefab = CreateNetworkObjectPrefab("OnwershipPrefab");
7386
m_OwnershipPrefab.AddComponent<NetworkObjectOwnershipComponent>();
87+
m_OwnershipPrefab.AddComponent<NetworkTransform>();
7488
if (m_DistributedAuthority)
7589
{
7690
m_OwnershipPrefab.GetComponent<NetworkObject>().SetOwnershipStatus(NetworkObject.OwnershipStatus.Transferable);
@@ -465,7 +479,87 @@ public IEnumerator TestOwnedObjectCounts()
465479

466480
yield return WaitForConditionOrTimeOut(ServerHasCorrectClientOwnedObjectCount);
467481
AssertOnTimeout($"Server does not have the correct count for all clients spawned {k_NumberOfSpawnedObjects} {nameof(NetworkObject)}s!");
482+
}
468483

484+
/// <summary>
485+
/// Validates that when changing ownership NetworkTransform does not enter into a bad state
486+
/// because the previous and current owner identifiers are the same. For client-server this
487+
/// ends up always being the server, but for distributed authority the authority changes when
488+
/// ownership changes.
489+
/// </summary>
490+
[UnityTest]
491+
public IEnumerator TestAuthorityChangingOwnership()
492+
{
493+
var authorityManager = (NetworkManager)null;
494+
var allNetworkManagers = m_ClientNetworkManagers.ToList();
495+
allNetworkManagers.Add(m_ServerNetworkManager);
496+
497+
if (m_DistributedAuthority)
498+
{
499+
var authorityId = Random.Range(1, NumberOfClients) - 1;
500+
authorityManager = m_ClientNetworkManagers[authorityId];
501+
m_OwnershipObject = SpawnObject(m_OwnershipPrefab, authorityManager);
502+
m_OwnershipNetworkObject = m_OwnershipObject.GetComponent<NetworkObject>();
503+
}
504+
else
505+
{
506+
authorityManager = m_ServerNetworkManager;
507+
m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager);
508+
m_OwnershipNetworkObject = m_OwnershipObject.GetComponent<NetworkObject>();
509+
}
510+
var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId;
511+
bool WaitForClientsToSpawnNetworkObject()
512+
{
513+
foreach (var clientNetworkManager in m_ClientNetworkManagers)
514+
{
515+
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
516+
{
517+
return false;
518+
}
519+
}
520+
return true;
521+
}
522+
523+
yield return WaitForConditionOrTimeOut(WaitForClientsToSpawnNetworkObject);
524+
AssertOnTimeout($"Timed out waiting for all clients to spawn the {m_OwnershipNetworkObject.name} {nameof(NetworkObject)} instance!");
525+
526+
var currentTargetOwner = (ulong)0;
527+
bool WaitForAllInstancesToChangeOwnership()
528+
{
529+
foreach (var clientNetworkManager in m_ClientNetworkManagers)
530+
{
531+
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
532+
{
533+
return false;
534+
}
535+
if (clientNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId].OwnerClientId != currentTargetOwner)
536+
{
537+
return false;
538+
}
539+
}
540+
return true;
541+
}
542+
543+
// Change ownership a few times and as long as the previous and current owners are not the same when
544+
// OnOwnershipChanged is invoked then the test passed.
545+
foreach (var networkManager in allNetworkManagers)
546+
{
547+
if (networkManager == authorityManager)
548+
{
549+
continue;
550+
}
551+
var clonedObject = networkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId];
552+
553+
if (clonedObject.OwnerClientId == networkManager.LocalClientId)
554+
{
555+
continue;
556+
}
557+
558+
var testComponent = clonedObject.GetComponent<NetworkObjectOwnershipComponent>();
559+
testComponent.ChangeOwnershipRpc();
560+
yield return WaitForAllInstancesToChangeOwnership();
561+
AssertOnTimeout($"Timed out waiting for all instances to change ownership to Client-{networkManager.LocalClientId}!");
562+
}
469563
}
470564
}
471565
}

0 commit comments

Comments
 (0)