Skip to content

Commit d3545c8

Browse files
fix: authority changing ownership via rpc can result in the same previous and current clientid values [BackPort] (#3434)
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 a client used an RPC to change ownership. This includes an additional check for dirty states of any collections base NetworkVariables. ## Changelog - Fixed: Issue where when a client changes ownership via RPC the `NetworkBehaviour.OnOwnershipChanged` can result in identical previous and current owner identifiers. ## 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. --> ## Backport This is a backport of #3347.
1 parent 2a4ac2b commit d3545c8

File tree

10 files changed

+167
-17
lines changed

10 files changed

+167
-17
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
1212

1313
### Fixed
1414

15+
- Fixed issue where when a client changes ownership via RPC the `NetworkBehaviour.OnOwnershipChanged` can result in identical previous and current owner identifiers. (#3434)
16+
1517
### Changed
1618

1719
## [1.13.0] - 2025-04-29

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,14 +1091,23 @@ internal void MarkVariablesDirty(bool dirty)
10911091
}
10921092
}
10931093

1094-
internal void MarkOwnerReadVariablesDirty()
1094+
/// <summary>
1095+
/// For owner read permissions, when changing ownership we need to do a full synchronization
1096+
/// of all NetworkVariables that are owner read permission based since the owner is the only
1097+
/// instance that knows what the most current values are.
1098+
/// </summary>
1099+
internal void MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty()
10951100
{
10961101
for (int j = 0; j < NetworkVariableFields.Count; j++)
10971102
{
10981103
if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner)
10991104
{
11001105
NetworkVariableFields[j].SetDirty(true);
11011106
}
1107+
if (NetworkVariableFields[j].WritePerm == NetworkVariableWritePermission.Owner)
1108+
{
1109+
NetworkVariableFields[j].OnCheckIsDirtyState();
1110+
}
11021111
}
11031112
}
11041113

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ internal void NetworkBehaviourUpdate(bool forceSend = false)
114114
{
115115
behaviour.PostNetworkVariableWrite(forceSend);
116116
}
117-
118-
// Once done processing, we set the previous owner id to the current owner id
119-
dirtyObj.PreviousOwnerId = dirtyObj.OwnerClientId;
120117
}
121118
m_DirtyNetworkObjects.Clear();
122119
}

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,12 +1485,39 @@ internal void MarkVariablesDirty(bool dirty)
14851485
}
14861486
}
14871487

1488-
internal void MarkOwnerReadVariablesDirty()
1488+
/// <summary>
1489+
/// Used when changing ownership, this will mark any owner read permission base NetworkVariables as dirty
1490+
/// and will check if any owner write permission NetworkVariables are dirty (primarily for collections) so
1491+
/// the new owner will get a full state update prior to changing ownership.
1492+
/// </summary>
1493+
/// <remarks>
1494+
/// We have to pass in the original owner and previous owner to "reset" back to the current state of this
1495+
/// NetworkObject in order to preserve the same ownership change flow. By the time this is invoked, the
1496+
/// new and previous owner ids have already been set.
1497+
/// </remarks>
1498+
/// <param name="originalOwnerId">the owner prior to beginning the change in ownership change.</param>
1499+
/// <param name="originalPreviousOwnerId">the previous owner prior to beginning the change in ownership change.</param>
1500+
internal void SynchronizeOwnerNetworkVariables(ulong originalOwnerId, ulong originalPreviousOwnerId)
14891501
{
1502+
var currentOwnerId = OwnerClientId;
1503+
OwnerClientId = originalOwnerId;
1504+
PreviousOwnerId = originalPreviousOwnerId;
14901505
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
14911506
{
1492-
ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty();
1507+
ChildNetworkBehaviours[i].MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty();
14931508
}
1509+
1510+
// Now set the new owner and previous owner identifiers back to their original new values
1511+
// before we run the NetworkBehaviourUpdate. For owner read only permissions this order of
1512+
// operations is **particularly important** as we need to first (above) mark things as dirty
1513+
// from the context of the original owner and then second (below) we need to send the messages
1514+
// which requires the new owner to be set for owner read permission NetworkVariables.
1515+
OwnerClientId = currentOwnerId;
1516+
PreviousOwnerId = originalOwnerId;
1517+
1518+
// Force send a state update for all owner read NetworkVariables and any currently dirty
1519+
// owner write NetworkVariables.
1520+
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
14941521
}
14951522

14961523
// 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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,7 @@ public void Handle(ref NetworkContext context)
6262

6363
if (originalOwner == networkManager.LocalClientId)
6464
{
65-
// Mark any owner read variables as dirty
66-
networkObject.MarkOwnerReadVariablesDirty();
67-
// Immediately queue any pending deltas and order the message before the
68-
// change in ownership message.
69-
networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
65+
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, networkObject.PreviousOwnerId);
7066
}
7167

7268
networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId);

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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,15 @@ internal ulong OwnerClientId()
345345
return m_NetworkBehaviour.NetworkObject.OwnerClientId;
346346
}
347347

348+
/// <summary>
349+
/// Primarily to check for collections dirty states when doing
350+
/// a fully owner read/write NetworkVariable update.
351+
/// </summary>
352+
internal virtual void OnCheckIsDirtyState()
353+
{
354+
355+
}
356+
348357
/// <summary>
349358
/// Writes the dirty changes, that is, the changes since the variable was last dirty, to the writer
350359
/// </summary>

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,17 @@ internal void RemoveOwnership(NetworkObject networkObject)
250250

251251
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
252252
{
253+
254+
if (clientId == networkObject.OwnerClientId)
255+
{
256+
if (NetworkManager.LogLevel <= LogLevel.Developer)
257+
{
258+
Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)");
259+
260+
}
261+
return;
262+
}
263+
253264
// If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes,
254265
// then notify the user they could potentially lose state updates if developer logging is enabled.
255266
if (m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup)
@@ -278,6 +289,8 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
278289
{
279290
throw new SpawnStateException("Object is not spawned");
280291
}
292+
var originalPreviousOwnerId = networkObject.PreviousOwnerId;
293+
var originalOwner = networkObject.OwnerClientId;
281294

282295
// Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates
283296
networkObject.PreviousOwnerId = networkObject.OwnerClientId;
@@ -294,13 +307,10 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
294307
// Always notify locally on the server when a new owner is assigned
295308
networkObject.InvokeBehaviourOnGainedOwnership();
296309

310+
// If we are the original owner, then we want to synchronize owner read & write NetworkVariables.
297311
if (networkObject.PreviousOwnerId == NetworkManager.LocalClientId)
298312
{
299-
// Mark any owner read variables as dirty
300-
networkObject.MarkOwnerReadVariablesDirty();
301-
// Immediately queue any pending deltas and order the message before the
302-
// change in ownership message.
303-
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
313+
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId);
304314
}
305315

306316
var message = new ChangeOwnershipMessage

com.unity.netcode.gameobjects/Tests/Runtime/DeferredMessagingTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1269,7 +1269,9 @@ public void WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNot
12691269
Assert.AreEqual(0, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject2.GetComponent<NetworkObject>().NetworkObjectId));
12701270
}
12711271

1272-
serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ServerNetworkManager.LocalClientId);
1272+
// Changing ownership when the owner specified is already an owner should not send any messages
1273+
// The original test was changing ownership to the server when the object was spawned with the server being an owner.
1274+
serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[1].LocalClientId);
12731275
WaitForAllClientsToReceive<ChangeOwnershipMessage>();
12741276

12751277
foreach (var client in m_ClientNetworkManagers)

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

Lines changed: 91 additions & 0 deletions
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;
@@ -13,21 +14,39 @@ public class NetworkObjectOwnershipComponent : NetworkBehaviour
1314
public bool OnLostOwnershipFired = false;
1415
public bool OnGainedOwnershipFired = false;
1516

17+
18+
/// <inheritdoc/>
1619
public override void OnLostOwnership()
1720
{
1821
OnLostOwnershipFired = true;
1922
}
2023

24+
25+
/// <inheritdoc/>
2126
public override void OnGainedOwnership()
2227
{
2328
OnGainedOwnershipFired = true;
2429
}
2530

31+
32+
/// <inheritdoc/>
33+
protected override void OnOwnershipChanged(ulong previous, ulong current)
34+
{
35+
Assert.True(previous != current, $"[{nameof(OnOwnershipChanged)}][Invalid Parameters] Invoked and the previous ({previous}) equals the current ({current})!");
36+
base.OnOwnershipChanged(previous, current);
37+
}
38+
2639
public void ResetFlags()
2740
{
2841
OnLostOwnershipFired = false;
2942
OnGainedOwnershipFired = false;
3043
}
44+
45+
[Rpc(SendTo.Server)]
46+
internal void ChangeOwnershipRpc(RpcParams rpcParams = default)
47+
{
48+
NetworkObject.ChangeOwnership(rpcParams.Receive.SenderClientId);
49+
}
3150
}
3251

3352
[TestFixture(HostOrServer.Host)]
@@ -52,6 +71,7 @@ protected override void OnServerAndClientsCreated()
5271
{
5372
m_OwnershipPrefab = CreateNetworkObjectPrefab("OnwershipPrefab");
5473
m_OwnershipPrefab.AddComponent<NetworkObjectOwnershipComponent>();
74+
m_OwnershipPrefab.AddComponent<NetworkTransform>();
5575
base.OnServerAndClientsCreated();
5676
}
5777

@@ -358,5 +378,76 @@ public IEnumerator TestOwnedObjectCounts()
358378
AssertOnTimeout($"Server does not have the correct count for all clients spawned {k_NumberOfSpawnedObjects} {nameof(NetworkObject)}s!");
359379

360380
}
381+
382+
/// <summary>
383+
/// Validates that when changing ownership NetworkTransform does not enter into a bad state
384+
/// because the previous and current owner identifiers are the same. For client-server this
385+
/// ends up always being the server, but for distributed authority the authority changes when
386+
/// ownership changes.
387+
/// </summary>
388+
/// <returns><see cref="IEnumerator"/></returns>
389+
[UnityTest]
390+
public IEnumerator TestAuthorityChangingOwnership()
391+
{
392+
var authorityManager = m_ServerNetworkManager; ;
393+
var allNetworkManagers = m_ClientNetworkManagers.ToList();
394+
allNetworkManagers.Add(m_ServerNetworkManager);
395+
396+
m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager);
397+
m_OwnershipNetworkObject = m_OwnershipObject.GetComponent<NetworkObject>();
398+
var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId;
399+
bool WaitForClientsToSpawnNetworkObject()
400+
{
401+
foreach (var clientNetworkManager in m_ClientNetworkManagers)
402+
{
403+
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
404+
{
405+
return false;
406+
}
407+
}
408+
return true;
409+
}
410+
411+
yield return WaitForConditionOrTimeOut(WaitForClientsToSpawnNetworkObject);
412+
AssertOnTimeout($"Timed out waiting for all clients to spawn the {m_OwnershipNetworkObject.name} {nameof(NetworkObject)} instance!");
413+
414+
var currentTargetOwner = (ulong)0;
415+
bool WaitForAllInstancesToChangeOwnership()
416+
{
417+
foreach (var clientNetworkManager in m_ClientNetworkManagers)
418+
{
419+
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
420+
{
421+
return false;
422+
}
423+
if (clientNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId].OwnerClientId != currentTargetOwner)
424+
{
425+
return false;
426+
}
427+
}
428+
return true;
429+
}
430+
431+
// Change ownership a few times and as long as the previous and current owners are not the same when
432+
// OnOwnershipChanged is invoked then the test passed.
433+
foreach (var networkManager in allNetworkManagers)
434+
{
435+
if (networkManager == authorityManager)
436+
{
437+
continue;
438+
}
439+
var clonedObject = networkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId];
440+
441+
if (clonedObject.OwnerClientId == networkManager.LocalClientId)
442+
{
443+
continue;
444+
}
445+
446+
var testComponent = clonedObject.GetComponent<NetworkObjectOwnershipComponent>();
447+
testComponent.ChangeOwnershipRpc();
448+
yield return WaitForAllInstancesToChangeOwnership();
449+
AssertOnTimeout($"Timed out waiting for all instances to change ownership to Client-{networkManager.LocalClientId}!");
450+
}
451+
}
361452
}
362453
}

0 commit comments

Comments
 (0)