Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

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

### Changed

## [1.13.0] - 2025-04-29
Expand Down
11 changes: 10 additions & 1 deletion com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,14 +1091,23 @@ internal void MarkVariablesDirty(bool dirty)
}
}

internal void MarkOwnerReadVariablesDirty()
/// <summary>
/// For owner read permissions, when changing ownership we need to do a full synchronization
/// of all NetworkVariables that are owner read permission based since the owner is the only
/// instance that knows what the most current values are.
/// </summary>
internal void MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty()
{
for (int j = 0; j < NetworkVariableFields.Count; j++)
{
if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner)
{
NetworkVariableFields[j].SetDirty(true);
}
if (NetworkVariableFields[j].WritePerm == NetworkVariableWritePermission.Owner)
{
NetworkVariableFields[j].OnCheckIsDirtyState();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ internal void NetworkBehaviourUpdate(bool forceSend = false)
{
behaviour.PostNetworkVariableWrite(forceSend);
}

// Once done processing, we set the previous owner id to the current owner id
dirtyObj.PreviousOwnerId = dirtyObj.OwnerClientId;
}
m_DirtyNetworkObjects.Clear();
}
Expand Down
31 changes: 29 additions & 2 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1485,12 +1485,39 @@ internal void MarkVariablesDirty(bool dirty)
}
}

internal void MarkOwnerReadVariablesDirty()
/// <summary>
/// Used when changing ownership, this will mark any owner read permission base NetworkVariables as dirty
/// and will check if any owner write permission NetworkVariables are dirty (primarily for collections) so
/// the new owner will get a full state update prior to changing ownership.
/// </summary>
/// <remarks>
/// We have to pass in the original owner and previous owner to "reset" back to the current state of this
/// NetworkObject in order to preserve the same ownership change flow. By the time this is invoked, the
/// new and previous owner ids have already been set.
/// </remarks>
/// <param name="originalOwnerId">the owner prior to beginning the change in ownership change.</param>
/// <param name="originalPreviousOwnerId">the previous owner prior to beginning the change in ownership change.</param>
internal void SynchronizeOwnerNetworkVariables(ulong originalOwnerId, ulong originalPreviousOwnerId)
{
var currentOwnerId = OwnerClientId;
OwnerClientId = originalOwnerId;
PreviousOwnerId = originalPreviousOwnerId;
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty();
ChildNetworkBehaviours[i].MarkOwnerReadDirtyAndCheckOwnerWriteIsDirty();
}

// Now set the new owner and previous owner identifiers back to their original new values
// before we run the NetworkBehaviourUpdate. For owner read only permissions this order of
// operations is **particularly important** as we need to first (above) mark things as dirty
// from the context of the original owner and then second (below) we need to send the messages
// which requires the new owner to be set for owner read permission NetworkVariables.
OwnerClientId = currentOwnerId;
PreviousOwnerId = originalOwnerId;

// Force send a state update for all owner read NetworkVariables and any currently dirty
// owner write NetworkVariables.
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
}

// NGO currently guarantees that the client will receive spawn data for all objects in one network tick.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ public void Handle(ref NetworkContext context)

if (originalOwner == networkManager.LocalClientId)
{
// Mark any owner read variables as dirty
networkObject.MarkOwnerReadVariablesDirty();
// Immediately queue any pending deltas and order the message before the
// change in ownership message.
networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, networkObject.PreviousOwnerId);
}

networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ public bool CheckDirtyState(bool forceCheck = false)
return isDirty;
}

/// <inheritdoc/>
internal override void OnCheckIsDirtyState()
{
CheckDirtyState();
base.OnCheckIsDirtyState();
}

internal ref T RefValue()
{
return ref m_InternalValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,15 @@ internal ulong OwnerClientId()
return m_NetworkBehaviour.NetworkObject.OwnerClientId;
}

/// <summary>
/// Primarily to check for collections dirty states when doing
/// a fully owner read/write NetworkVariable update.
/// </summary>
internal virtual void OnCheckIsDirtyState()
{

}

/// <summary>
/// Writes the dirty changes, that is, the changes since the variable was last dirty, to the writer
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ internal void RemoveOwnership(NetworkObject networkObject)

internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
{

if (clientId == networkObject.OwnerClientId)
{
if (NetworkManager.LogLevel <= LogLevel.Developer)
{
Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)");

}
return;
}

// If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes,
// then notify the user they could potentially lose state updates if developer logging is enabled.
if (m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup)
Expand Down Expand Up @@ -278,6 +289,8 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
{
throw new SpawnStateException("Object is not spawned");
}
var originalPreviousOwnerId = networkObject.PreviousOwnerId;
var originalOwner = networkObject.OwnerClientId;

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

// If we are the original owner, then we want to synchronize owner read & write NetworkVariables.
if (networkObject.PreviousOwnerId == NetworkManager.LocalClientId)
{
// Mark any owner read variables as dirty
networkObject.MarkOwnerReadVariablesDirty();
// Immediately queue any pending deltas and order the message before the
// change in ownership message.
NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId);
}

var message = new ChangeOwnershipMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
Expand All @@ -13,21 +14,39 @@ public class NetworkObjectOwnershipComponent : NetworkBehaviour
public bool OnLostOwnershipFired = false;
public bool OnGainedOwnershipFired = false;


/// <inheritdoc/>
public override void OnLostOwnership()
{
OnLostOwnershipFired = true;
}


/// <inheritdoc/>
public override void OnGainedOwnership()
{
OnGainedOwnershipFired = true;
}


/// <inheritdoc/>
protected override void OnOwnershipChanged(ulong previous, ulong current)
{
Assert.True(previous != current, $"[{nameof(OnOwnershipChanged)}][Invalid Parameters] Invoked and the previous ({previous}) equals the current ({current})!");
base.OnOwnershipChanged(previous, current);
}

public void ResetFlags()
{
OnLostOwnershipFired = false;
OnGainedOwnershipFired = false;
}

[Rpc(SendTo.Server)]
internal void ChangeOwnershipRpc(RpcParams rpcParams = default)
{
NetworkObject.ChangeOwnership(rpcParams.Receive.SenderClientId);
}
}

[TestFixture(HostOrServer.Host)]
Expand All @@ -52,6 +71,7 @@ protected override void OnServerAndClientsCreated()
{
m_OwnershipPrefab = CreateNetworkObjectPrefab("OnwershipPrefab");
m_OwnershipPrefab.AddComponent<NetworkObjectOwnershipComponent>();
m_OwnershipPrefab.AddComponent<NetworkTransform>();
base.OnServerAndClientsCreated();
}

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

}

/// <summary>
/// Validates that when changing ownership NetworkTransform does not enter into a bad state
/// because the previous and current owner identifiers are the same. For client-server this
/// ends up always being the server, but for distributed authority the authority changes when
/// ownership changes.
/// </summary>
/// <returns><see cref="IEnumerator"/></returns>
[UnityTest]
public IEnumerator TestAuthorityChangingOwnership()
{
var authorityManager = m_ServerNetworkManager; ;
var allNetworkManagers = m_ClientNetworkManagers.ToList();
allNetworkManagers.Add(m_ServerNetworkManager);

m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager);
m_OwnershipNetworkObject = m_OwnershipObject.GetComponent<NetworkObject>();
var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId;
bool WaitForClientsToSpawnNetworkObject()
{
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
{
return false;
}
}
return true;
}

yield return WaitForConditionOrTimeOut(WaitForClientsToSpawnNetworkObject);
AssertOnTimeout($"Timed out waiting for all clients to spawn the {m_OwnershipNetworkObject.name} {nameof(NetworkObject)} instance!");

var currentTargetOwner = (ulong)0;
bool WaitForAllInstancesToChangeOwnership()
{
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId))
{
return false;
}
if (clientNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId].OwnerClientId != currentTargetOwner)
{
return false;
}
}
return true;
}

// Change ownership a few times and as long as the previous and current owners are not the same when
// OnOwnershipChanged is invoked then the test passed.
foreach (var networkManager in allNetworkManagers)
{
if (networkManager == authorityManager)
{
continue;
}
var clonedObject = networkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId];

if (clonedObject.OwnerClientId == networkManager.LocalClientId)
{
continue;
}

var testComponent = clonedObject.GetComponent<NetworkObjectOwnershipComponent>();
testComponent.ChangeOwnershipRpc();
yield return WaitForAllInstancesToChangeOwnership();
AssertOnTimeout($"Timed out waiting for all instances to change ownership to Client-{networkManager.LocalClientId}!");
}
}
}
}