From 76a0480f52aabac725bd960db1e0f944b9763cd5 Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Mon, 23 Mar 2026 16:52:46 +0100 Subject: [PATCH 1/9] styling + is spawned check in internal has authory --- .../Runtime/Core/NetworkObject.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index c667af47db..05df0eabc0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -390,7 +390,7 @@ private void CheckForInScenePlaced() /// Defaults to true, determines whether the will be destroyed. public void DeferDespawn(int tickOffset, bool destroy = true) { - // Ensure we log the DAMode message first as locking ownership is not allowed if not DA so the DA message is the most relevant. + // The DAMode message is logged first, as ownership locking isn’t allowed when not in DAMode, making it the most relevant message. if (!NetworkManager.DistributedAuthorityMode) { if (NetworkManager.LogLevel <= LogLevel.Error) @@ -606,7 +606,7 @@ internal void RemoveOwnershipExtended(OwnershipStatusExtended extended) /// true or false depending upon lock operation's success public bool SetOwnershipLock(bool lockOwnership = true) { - // Ensure we log the DAMode message first as locking ownership is not allowed if not DA so the DA message is the most relevant. + // The DAMode message is logged first, as ownership locking isn’t allowed when not in DAMode, making it the most relevant message. if (!NetworkManager.DistributedAuthorityMode) { if (NetworkManager.LogLevel <= LogLevel.Error) @@ -1154,8 +1154,11 @@ public bool HasOwnershipStatus(OwnershipStatus status) [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool InternalHasAuthority() { - var networkManager = NetworkManager; - return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; + if (!IsSpawned) + { + return false; + } + return NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; } /// @@ -3541,7 +3544,7 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes - if (isAuthority && notify && !transform.parent) + if (isAuthority && notify && transform.parent == null) { NetworkManagerOwner.SceneManager.NotifyNetworkObjectSceneChanged(this); } From d9c9211a9daffcb2fbe241c1e992d45bb3812c27 Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Wed, 10 Jun 2026 15:46:04 +0200 Subject: [PATCH 2/9] Use InternalHasAuthority instead of public field --- .../Runtime/Core/NetworkBehaviour.cs | 2 +- .../Runtime/Core/NetworkObject.cs | 28 ++++++++----------- .../DefaultSceneManagerHandler.cs | 4 +-- .../SceneManagement/NetworkSceneManager.cs | 6 ++-- .../Runtime/Spawning/NetworkSpawnManager.cs | 8 +++--- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 16b6d3be62..ab92df14c3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -670,7 +670,7 @@ internal void UpdateNetworkProperties() IsClient = m_NetworkManager.IsListening && m_NetworkManager.IsClient; IsServer = m_NetworkManager.IsListening && m_NetworkManager.IsServer; IsSessionOwner = m_NetworkManager.IsListening && m_NetworkManager.LocalClient.IsSessionOwner; - HasAuthority = m_NetworkObject.HasAuthority; + HasAuthority = m_NetworkObject.InternalHasAuthority(); ServerIsHost = m_NetworkManager.IsListening && m_NetworkManager.ServerIsHost; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 47570b74e6..cb0b23006b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -402,7 +402,6 @@ public void DeferDespawn(int tickOffset, bool destroy = true) { NetworkLog.LogErrorServer($"[{name}] This method is only available in distributed authority mode."); } - return; } @@ -412,11 +411,10 @@ public void DeferDespawn(int tickOffset, bool destroy = true) { NetworkLog.LogErrorServer($"[{name}] Cannot defer despawn while not spawned."); } - return; } - if (!HasAuthority) + if (!InternalHasAuthority()) { if (NetworkManagerOwner.LogLevel <= LogLevel.Error) { @@ -628,12 +626,11 @@ public bool SetOwnershipLock(bool lockOwnership = true) { NetworkLog.LogErrorServer($"[{name}][Attempted Lock While not spawned]"); } - return false; } // If we don't have authority exit early - if (!HasAuthority) + if (!InternalHasAuthority()) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -671,7 +668,6 @@ public bool SetOwnershipLock(bool lockOwnership = true) { SendOwnershipStatusUpdate(); } - return true; } @@ -797,7 +793,6 @@ public OwnershipRequestStatus RequestOwnership() { NetworkLog.LogErrorServer($"[{name}][Invalid Operation] Cannot request ownership of an NetworkObject before it is spawned."); } - return OwnershipRequestStatus.InvalidOperation; } // Exit early the local client is already the owner @@ -909,7 +904,7 @@ internal void OwnershipRequest(ulong clientRequestingOwnership) // This action is always authorized as long as the client still has authority. // We need to pass in that this is a request approval ownership change. - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, HasAuthority, true); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, InternalHasAuthority(), true); } else { @@ -1158,7 +1153,7 @@ public bool HasOwnershipStatus(OwnershipStatus status) public bool HasAuthority => InternalHasAuthority(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool InternalHasAuthority() + internal bool InternalHasAuthority() { if (!IsSpawned) { @@ -1509,7 +1504,7 @@ public void NetworkShow(ulong clientId) return; } - if (!HasAuthority) + if (!InternalHasAuthority()) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1604,7 +1599,7 @@ public void NetworkHide(ulong clientId) return; } - if (!HasAuthority) + if (!InternalHasAuthority()) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1763,7 +1758,7 @@ private void OnDestroy() { // An authorized destroy is when done by the authority instance or done due to a scene event and the NetworkObject // was marked as destroy pending scene event (which means the destroy with scene property was set). - var isAuthorityDestroy = HasAuthority || NetworkManager.DAHost || DestroyPendingSceneEvent; + var isAuthorityDestroy = InternalHasAuthority() || NetworkManager.DAHost || DestroyPendingSceneEvent; // If the NetworkObject's GameObject is still valid and the scene is still valid and loaded, then we are still valid var isStillValid = gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded; @@ -2102,7 +2097,7 @@ public void ChangeOwnership(ulong newOwnerClientId) } return; } - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, HasAuthority); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, InternalHasAuthority()); } /// @@ -2337,7 +2332,7 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) // DANGO-TODO: Do we want to worry about ownership permissions here? // It wouldn't make sense to not allow parenting, but keeping this note here as a reminder. - var isAuthority = HasAuthority || (AllowOwnerToParent && IsOwner); + var isAuthority = InternalHasAuthority() || (AllowOwnerToParent && IsOwner); // If we don't have authority and we are not shutting down, then don't allow any parenting. // If we are shutting down and don't have authority then allow it. @@ -2412,7 +2407,7 @@ private void OnTransformParentChanged() // With distributed authority, we need to track "valid authoritative" parenting changes. // So, either the authority or AuthorityAppliedParenting is considered a "valid parenting change". - var isParentingAuthority = HasAuthority || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); + var isParentingAuthority = InternalHasAuthority() || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); // If we are spawned and don't have authority; reset the parent back to the cached parent and exit if (!isParentingAuthority) { @@ -3407,7 +3402,6 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, { Destroy(networkObject.gameObject); } - return null; } @@ -3529,7 +3523,7 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) return; } - var isAuthority = HasAuthority; + var isAuthority = InternalHasAuthority(); SceneOriginHandle = scene.handle; // non-authority needs to update the NetworkSceneHandle diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs index 76ea8feb04..c4041b18c1 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs @@ -302,7 +302,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa } // Check to determine if we need to allow destroying a non-authority instance - if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority) + if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.InternalHasAuthority()) { networkObject.DestroyPendingSceneEvent = true; } @@ -316,7 +316,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } } - else if (networkObject.HasAuthority) + else if (networkObject.InternalHasAuthority()) { // We know this instance is going to be destroyed (when it receives the destroy object message). // We have to invoke this prior to invoking despawn in order to know that we are de-spawning in diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 8c07fbd603..1a4fa0d146 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -2713,7 +2713,7 @@ internal void MoveObjectsToDontDestroyOnLoad() } // Check to determine if we need to allow destroying a non-authority instance - if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority) + if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.InternalHasAuthority()) { networkObject.DestroyPendingSceneEvent = true; } @@ -2731,7 +2731,7 @@ internal void MoveObjectsToDontDestroyOnLoad() networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; } } - else if (networkObject.HasAuthority) + else if (networkObject.InternalHasAuthority()) { networkObject.SetIsDestroying(); // Only destroy non-scene placed NetworkObjects to avoid warnings about destroying in-scene placed NetworkObjects. @@ -2889,7 +2889,7 @@ internal bool IsSceneUnloading(NetworkObject networkObject) internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) { // Really, this should never happen but in case it does - if (!networkObject.HasAuthority) + if (!networkObject.InternalHasAuthority()) { if (NetworkManager.LogLevel == LogLevel.Developer) { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 46f52bc4f9..e868424998 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1574,7 +1574,7 @@ internal void ServerDestroySpawnedSceneObjects() if (networkObject.InScenePlaced && networkObject.DestroyWithScene && networkObject.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) { - if (networkObject.IsSpawned && networkObject.HasAuthority) + if (networkObject.IsSpawned && networkObject.InternalHasAuthority()) { networkObject.Despawn(false); } @@ -1729,7 +1729,7 @@ internal void ServerSpawnSceneObjectsOnStartSweep() /// internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject, bool destroyGameObject) { - if (networkObject.HasAuthority) + if (networkObject.InternalHasAuthority()) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -1806,7 +1806,7 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro } // For mixed authority hierarchies, if the parent is despawned then any removal of children // is considered "authority approved". Set the AuthorityAppliedParenting flag. - spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !networkObject.HasAuthority; + spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !networkObject.InternalHasAuthority(); // Try to remove the parent using the cached WorldPositionStays value // Note: WorldPositionStays will still default to true if this was an @@ -1834,7 +1834,7 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro networkObject.InvokeBehaviourNetworkDespawn(); // Whether we are in distributedAuthority mode and have authority on this object - var hasDAAuthority = distributedAuthority && (networkObject.HasAuthority || (NetworkManager.DAHost && authorityOverride)); + var hasDAAuthority = distributedAuthority && (networkObject.InternalHasAuthority() || (NetworkManager.DAHost && authorityOverride)); // Don't send messages if shutting down // Otherwise send messages if we are the authority (either the server, or the DA mode authority of this object). From f5ea5cbb18b07dc514d0710332c45c1a0494679f Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Thu, 11 Jun 2026 11:13:50 +0200 Subject: [PATCH 3/9] Revert IsSpawn check --- .../Runtime/Core/NetworkObject.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index cb0b23006b..affe436e80 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -861,7 +861,7 @@ public OwnershipRequestStatus RequestOwnership() public OnOwnershipRequestedDelegateHandler OnOwnershipRequested; /// - /// Invoked by ChangeOwnershipMessage + /// Invoked by /// /// the client requesting ownership internal void OwnershipRequest(ulong clientRequestingOwnership) @@ -1155,11 +1155,8 @@ public bool HasOwnershipStatus(OwnershipStatus status) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool InternalHasAuthority() { - if (!IsSpawned) - { - return false; - } - return NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + var networkManager = NetworkManager; + return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; } /// From 75b7480d0f1a0448fb3b90071b0b3e6d770b1a14 Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Thu, 11 Jun 2026 11:36:53 +0200 Subject: [PATCH 4/9] Revert private to internal change --- .../Runtime/Core/NetworkBehaviour.cs | 2 +- .../Runtime/Core/NetworkObject.cs | 2 +- .../Runtime/SceneManagement/DefaultSceneManagerHandler.cs | 4 ++-- .../Runtime/SceneManagement/NetworkSceneManager.cs | 6 +++--- .../Runtime/Spawning/NetworkSpawnManager.cs | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index ab92df14c3..16b6d3be62 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -670,7 +670,7 @@ internal void UpdateNetworkProperties() IsClient = m_NetworkManager.IsListening && m_NetworkManager.IsClient; IsServer = m_NetworkManager.IsListening && m_NetworkManager.IsServer; IsSessionOwner = m_NetworkManager.IsListening && m_NetworkManager.LocalClient.IsSessionOwner; - HasAuthority = m_NetworkObject.InternalHasAuthority(); + HasAuthority = m_NetworkObject.HasAuthority; ServerIsHost = m_NetworkManager.IsListening && m_NetworkManager.ServerIsHost; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index affe436e80..744b142d96 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1153,7 +1153,7 @@ public bool HasOwnershipStatus(OwnershipStatus status) public bool HasAuthority => InternalHasAuthority(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool InternalHasAuthority() + private bool InternalHasAuthority() { var networkManager = NetworkManager; return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs index c4041b18c1..76ea8feb04 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs @@ -302,7 +302,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa } // Check to determine if we need to allow destroying a non-authority instance - if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.InternalHasAuthority()) + if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority) { networkObject.DestroyPendingSceneEvent = true; } @@ -316,7 +316,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } } - else if (networkObject.InternalHasAuthority()) + else if (networkObject.HasAuthority) { // We know this instance is going to be destroyed (when it receives the destroy object message). // We have to invoke this prior to invoking despawn in order to know that we are de-spawning in diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 1a4fa0d146..8c07fbd603 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -2713,7 +2713,7 @@ internal void MoveObjectsToDontDestroyOnLoad() } // Check to determine if we need to allow destroying a non-authority instance - if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.InternalHasAuthority()) + if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority) { networkObject.DestroyPendingSceneEvent = true; } @@ -2731,7 +2731,7 @@ internal void MoveObjectsToDontDestroyOnLoad() networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; } } - else if (networkObject.InternalHasAuthority()) + else if (networkObject.HasAuthority) { networkObject.SetIsDestroying(); // Only destroy non-scene placed NetworkObjects to avoid warnings about destroying in-scene placed NetworkObjects. @@ -2889,7 +2889,7 @@ internal bool IsSceneUnloading(NetworkObject networkObject) internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) { // Really, this should never happen but in case it does - if (!networkObject.InternalHasAuthority()) + if (!networkObject.HasAuthority) { if (NetworkManager.LogLevel == LogLevel.Developer) { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index e868424998..46f52bc4f9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1574,7 +1574,7 @@ internal void ServerDestroySpawnedSceneObjects() if (networkObject.InScenePlaced && networkObject.DestroyWithScene && networkObject.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) { - if (networkObject.IsSpawned && networkObject.InternalHasAuthority()) + if (networkObject.IsSpawned && networkObject.HasAuthority) { networkObject.Despawn(false); } @@ -1729,7 +1729,7 @@ internal void ServerSpawnSceneObjectsOnStartSweep() /// internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject, bool destroyGameObject) { - if (networkObject.InternalHasAuthority()) + if (networkObject.HasAuthority) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -1806,7 +1806,7 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro } // For mixed authority hierarchies, if the parent is despawned then any removal of children // is considered "authority approved". Set the AuthorityAppliedParenting flag. - spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !networkObject.InternalHasAuthority(); + spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !networkObject.HasAuthority; // Try to remove the parent using the cached WorldPositionStays value // Note: WorldPositionStays will still default to true if this was an @@ -1834,7 +1834,7 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro networkObject.InvokeBehaviourNetworkDespawn(); // Whether we are in distributedAuthority mode and have authority on this object - var hasDAAuthority = distributedAuthority && (networkObject.InternalHasAuthority() || (NetworkManager.DAHost && authorityOverride)); + var hasDAAuthority = distributedAuthority && (networkObject.HasAuthority || (NetworkManager.DAHost && authorityOverride)); // Don't send messages if shutting down // Otherwise send messages if we are the authority (either the server, or the DA mode authority of this object). From 84fe50ffd9476d11f38b77b7f6659f63d590e54f Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Mon, 15 Jun 2026 13:27:07 +0200 Subject: [PATCH 5/9] Revert cleanup - to move to other branch --- .../Runtime/Core/NetworkObject.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 744b142d96..f73b187945 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -395,13 +395,14 @@ private void CheckForInScenePlaced() /// Defaults to true, determines whether the will be destroyed. public void DeferDespawn(int tickOffset, bool destroy = true) { - // The DAMode message is logged first, as ownership locking isn’t allowed when not in DAMode, making it the most relevant message. + // Ensure we log the DAMode message first as locking ownership is not allowed if not DA so the DA message is the most relevant. if (!NetworkManager.DistributedAuthorityMode) { if (NetworkManager.LogLevel <= LogLevel.Error) { NetworkLog.LogErrorServer($"[{name}] This method is only available in distributed authority mode."); } + return; } @@ -411,6 +412,7 @@ public void DeferDespawn(int tickOffset, bool destroy = true) { NetworkLog.LogErrorServer($"[{name}] Cannot defer despawn while not spawned."); } + return; } @@ -610,7 +612,7 @@ internal void RemoveOwnershipExtended(OwnershipStatusExtended extended) /// true or false depending upon lock operation's success public bool SetOwnershipLock(bool lockOwnership = true) { - // The DAMode message is logged first, as ownership locking isn’t allowed when not in DAMode, making it the most relevant message. + // Ensure we log the DAMode message first as locking ownership is not allowed if not DA so the DA message is the most relevant. if (!NetworkManager.DistributedAuthorityMode) { if (NetworkManager.LogLevel <= LogLevel.Error) @@ -626,6 +628,7 @@ public bool SetOwnershipLock(bool lockOwnership = true) { NetworkLog.LogErrorServer($"[{name}][Attempted Lock While not spawned]"); } + return false; } @@ -668,6 +671,7 @@ public bool SetOwnershipLock(bool lockOwnership = true) { SendOwnershipStatusUpdate(); } + return true; } @@ -793,6 +797,7 @@ public OwnershipRequestStatus RequestOwnership() { NetworkLog.LogErrorServer($"[{name}][Invalid Operation] Cannot request ownership of an NetworkObject before it is spawned."); } + return OwnershipRequestStatus.InvalidOperation; } // Exit early the local client is already the owner @@ -861,7 +866,7 @@ public OwnershipRequestStatus RequestOwnership() public OnOwnershipRequestedDelegateHandler OnOwnershipRequested; /// - /// Invoked by + /// Invoked by ChangeOwnershipMessage /// /// the client requesting ownership internal void OwnershipRequest(ulong clientRequestingOwnership) @@ -3399,6 +3404,7 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, { Destroy(networkObject.gameObject); } + return null; } @@ -3555,7 +3561,7 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes - if (isAuthority && notify && transform.parent == null) + if (isAuthority && notify && !transform.parent) { NetworkManagerOwner.SceneManager.NotifyNetworkObjectSceneChanged(this); } From 51fa3661d3b8090067aabd840467e10da22a57d2 Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Mon, 15 Jun 2026 17:50:09 +0200 Subject: [PATCH 6/9] Convert HasAuthority method into a field Update it on Spawn, InvokeBehaviourOnOwnershipChanged and ResetOnDespawn --- .../Runtime/Core/NetworkObject.cs | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index f73b187945..3975888ab4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -416,7 +416,7 @@ public void DeferDespawn(int tickOffset, bool destroy = true) return; } - if (!InternalHasAuthority()) + if (!m_HasAuthority) { if (NetworkManagerOwner.LogLevel <= LogLevel.Error) { @@ -633,7 +633,7 @@ public bool SetOwnershipLock(bool lockOwnership = true) } // If we don't have authority exit early - if (!InternalHasAuthority()) + if (!m_HasAuthority) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -909,7 +909,7 @@ internal void OwnershipRequest(ulong clientRequestingOwnership) // This action is always authorized as long as the client still has authority. // We need to pass in that this is a request approval ownership change. - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, InternalHasAuthority(), true); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, m_HasAuthority, true); } else { @@ -1155,14 +1155,9 @@ public bool HasOwnershipStatus(OwnershipStatus status) /// /// When in client-server mode, authority should is not considered the same as ownership. /// - public bool HasAuthority => InternalHasAuthority(); + public bool HasAuthority => IsSpawned ? m_HasAuthority : NetworkManager.IsServer; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool InternalHasAuthority() - { - var networkManager = NetworkManager; - return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; - } + private bool m_HasAuthority; /// /// The NetworkManager that owns this NetworkObject. @@ -1506,7 +1501,7 @@ public void NetworkShow(ulong clientId) return; } - if (!InternalHasAuthority()) + if (!m_HasAuthority) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1601,7 +1596,7 @@ public void NetworkHide(ulong clientId) return; } - if (!InternalHasAuthority()) + if (!m_HasAuthority) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1760,7 +1755,7 @@ private void OnDestroy() { // An authorized destroy is when done by the authority instance or done due to a scene event and the NetworkObject // was marked as destroy pending scene event (which means the destroy with scene property was set). - var isAuthorityDestroy = InternalHasAuthority() || NetworkManager.DAHost || DestroyPendingSceneEvent; + var isAuthorityDestroy = m_HasAuthority || NetworkManager.DAHost || DestroyPendingSceneEvent; // If the NetworkObject's GameObject is still valid and the scene is still valid and loaded, then we are still valid var isStillValid = gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded; @@ -2005,8 +2000,8 @@ public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ow /// Should the object be destroyed when the scene is changed public void Spawn(bool destroyWithScene = false) { - var networkManager = NetworkManager; - var clientId = networkManager.DistributedAuthorityMode ? networkManager.LocalClientId : NetworkManager.ServerClientId; + var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; + m_HasAuthority = NetworkManager.DistributedAuthorityMode ? OwnerClientId == NetworkManager.LocalClientId : NetworkManager.IsServer; SpawnInternal(destroyWithScene, clientId, false); } @@ -2062,6 +2057,7 @@ internal void ResetOnDespawn() { // Always clear out the observers list when despawned Observers.Clear(); + m_HasAuthority = false; IsSpawnAuthority = false; IsSpawned = false; DeferredDespawnTick = 0; @@ -2099,7 +2095,7 @@ public void ChangeOwnership(ulong newOwnerClientId) } return; } - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, InternalHasAuthority()); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, m_HasAuthority); } /// @@ -2122,6 +2118,8 @@ internal void InvokeBehaviourOnOwnershipChanged(ulong originalOwnerClientId, ulo var isPreviousOwner = originalOwnerClientId == NetworkManagerOwner.LocalClientId; var isNewOwner = newOwnerClientId == NetworkManagerOwner.LocalClientId; + m_HasAuthority = distributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + if (distributedAuthorityMode || isPreviousOwner) { NetworkManagerOwner.SpawnManager.UpdateOwnershipTable(this, originalOwnerClientId, true); @@ -2334,7 +2332,7 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) // DANGO-TODO: Do we want to worry about ownership permissions here? // It wouldn't make sense to not allow parenting, but keeping this note here as a reminder. - var isAuthority = InternalHasAuthority() || (AllowOwnerToParent && IsOwner); + var isAuthority = m_HasAuthority || (AllowOwnerToParent && IsOwner); // If we don't have authority and we are not shutting down, then don't allow any parenting. // If we are shutting down and don't have authority then allow it. @@ -2409,7 +2407,7 @@ private void OnTransformParentChanged() // With distributed authority, we need to track "valid authoritative" parenting changes. // So, either the authority or AuthorityAppliedParenting is considered a "valid parenting change". - var isParentingAuthority = InternalHasAuthority() || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); + var isParentingAuthority = m_HasAuthority || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); // If we are spawned and don't have authority; reset the parent back to the cached parent and exit if (!isParentingAuthority) { @@ -3526,15 +3524,14 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) return; } - var isAuthority = InternalHasAuthority(); SceneOriginHandle = scene.handle; // non-authority needs to update the NetworkSceneHandle - if (!isAuthority && NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle)) + if (!m_HasAuthority && NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle)) { NetworkSceneHandle = NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle]; } - else if (isAuthority) + else if (m_HasAuthority) { // Since the authority is the source of truth for the NetworkSceneHandle, // the NetworkSceneHandle is the same as the SceneOriginHandle. @@ -3561,7 +3558,7 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes - if (isAuthority && notify && !transform.parent) + if (m_HasAuthority && notify && !transform.parent) { NetworkManagerOwner.SceneManager.NotifyNetworkObjectSceneChanged(this); } From 76310778e95ee687bdd75970f412e4cc4b71512c Mon Sep 17 00:00:00 2001 From: Noellie Velez Date: Wed, 17 Jun 2026 19:39:57 +0200 Subject: [PATCH 7/9] move spawn setup into NetworkObject.SetupOnSpawn --- .../Runtime/Core/NetworkObject.cs | 72 ++++++++++++++++++- .../Runtime/Spawning/NetworkSpawnManager.cs | 70 ++---------------- 2 files changed, 76 insertions(+), 66 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3975888ab4..6b929fee06 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -2001,7 +2001,6 @@ public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ow public void Spawn(bool destroyWithScene = false) { var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; - m_HasAuthority = NetworkManager.DistributedAuthorityMode ? OwnerClientId == NetworkManager.LocalClientId : NetworkManager.IsServer; SpawnInternal(destroyWithScene, clientId, false); } @@ -2053,6 +2052,72 @@ public void Despawn(bool destroy = true) NetworkManagerOwner.SpawnManager.DespawnObject(this, destroy); } + internal void SetupOnSpawn(ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + { +#pragma warning disable CS0618 // Type or member is obsolete + // Obsolete with warning means we need the underlying behaviour to keep existing + // TODO: remove in the 3.x branch + SetSceneObjectStatus(sceneObject); +#pragma warning restore CS0618 // Type or member is obsolete + + // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects + // Note: Always check SceneOriginHandle directly at this specific location. + if (InScenePlaced && SceneOriginHandle.IsEmpty()) + { + SceneOrigin = gameObject.scene; + } + + NetworkObjectId = networkId; + + DestroyWithScene = sceneObject || destroyWithScene; + + IsPlayerObject = playerObject; + + OwnerClientId = ownerClientId; + + // When spawned, previous owner is always the first assigned owner + PreviousOwnerId = ownerClientId; + + // If this is the player and the client is the owner, then lock ownership by default + if (NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.LocalClientId == ownerClientId && playerObject) + { + AddOwnershipExtended(OwnershipStatusExtended.Locked); + } + + m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + IsSpawned = true; + + // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, + // then add all connected clients as observers + if (!NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.IsServer && SpawnWithObservers) + { + // If running as a server only, then make sure to always add the server's client identifier + if (!NetworkManagerOwner.IsHost) + { + AddObserver(NetworkManagerOwner.LocalClientId); + } + + // Add client observers + for (int i = 0; i < NetworkManagerOwner.ConnectedClientsIds.Count; i++) + { + // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. + if (CheckObjectVisibility != null && !CheckObjectVisibility(NetworkManagerOwner.ConnectedClientsIds[i])) + { + continue; + } + AddObserver(NetworkManagerOwner.ConnectedClientsIds[i]); + } + } + + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (InScenePlaced && InScenePlacedSourceGlobalObjectIdHash != 0) + { + PrefabGlobalObjectIdHash = InScenePlacedSourceGlobalObjectIdHash; + } + + } + internal void ResetOnDespawn() { // Always clear out the observers list when despawned @@ -2118,7 +2183,10 @@ internal void InvokeBehaviourOnOwnershipChanged(ulong originalOwnerClientId, ulo var isPreviousOwner = originalOwnerClientId == NetworkManagerOwner.LocalClientId; var isNewOwner = newOwnerClientId == NetworkManagerOwner.LocalClientId; - m_HasAuthority = distributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + if (distributedAuthorityMode) + { + m_HasAuthority = isNewOwner; + } if (distributedAuthorityMode || isPreviousOwner) { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 46f52bc4f9..95f1b3d305 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1295,9 +1295,10 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize } /// - /// Handles the all the final setup and spawning needed for + /// Handles all the final setup and spawning needed for spawning a NetworkObject locally. /// - /// boolean indicating whether the spawn succeeded. Internal dev note: THIS IS A CATCH FOR OURSELVES. DON'T PULL OUT + /// boolean indicating whether the spawn succeeded. + // Internal dev note: THIS IS A CATCH FOR OURSELVES. DON'T PULL OUT internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { // TODO: Replace the following checks with internal Netcode asserts @@ -1311,7 +1312,7 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong return false; } - if (networkId == default) + if (networkId == 0) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -1320,70 +1321,18 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong return false; } -#pragma warning disable CS0618 // Type or member is obsolete - // Obsolete with warning means we need the underlying behaviour to keep existing - // TODO: remove in the 3.x branch - networkObject.SetSceneObjectStatus(sceneObject); -#pragma warning restore CS0618 // Type or member is obsolete + networkObject.SetupOnSpawn(networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); - // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects - // Note: Always check SceneOriginHandle directly at this specific location. - if (networkObject.InScenePlaced && networkObject.SceneOriginHandle.IsEmpty()) - { - networkObject.SceneOrigin = networkObject.gameObject.scene; - } - - networkObject.NetworkObjectId = networkId; - - networkObject.DestroyWithScene = sceneObject || destroyWithScene; - - networkObject.IsPlayerObject = playerObject; - - networkObject.OwnerClientId = ownerClientId; - - // When spawned, previous owner is always the first assigned owner - networkObject.PreviousOwnerId = ownerClientId; - - // If this the player and the client is the owner, then lock ownership by default - if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) - { - networkObject.AddOwnershipExtended(NetworkObject.OwnershipStatusExtended.Locked); - } - - networkObject.IsSpawned = true; SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); SpawnedObjectsList.Add(networkObject); - - // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, - // then add all connected clients as observers - if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) - { - // If running as a server only, then make sure to always add the server's client identifier - if (!NetworkManager.IsHost) - { - networkObject.AddObserver(NetworkManager.LocalClientId); - } - - // Add client observers - for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) - { - // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) - { - continue; - } - networkObject.AddObserver(NetworkManager.ConnectedClientsIds[i]); - } - } - networkObject.ApplyNetworkParenting(); + NetworkObject.CheckOrphanChildren(); AddNetworkObjectToSceneChangedUpdates(networkObject); networkObject.InvokeBehaviourNetworkSpawn(); - // Only dynamically spawned NetworkObjects are allowed if (!sceneObject) { @@ -1395,13 +1344,6 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong UpdateNetworkClientPlayer(networkObject); } - // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set - // then assign this to the PrefabGlobalObjectIdHash - if (networkObject.InScenePlaced && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) - { - networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; - } - return true; } From 11c10e43202413277b0a2204cfa381b24a1b4f4d Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 23 Jun 2026 14:36:40 -0400 Subject: [PATCH 8/9] Refactor SetupOnSpawn --- .../Runtime/Core/NetworkObject.cs | 165 ++++++++++-------- .../Runtime/Logging/NetworkLog.cs | 13 ++ .../Runtime/Spawning/NetworkSpawnManager.cs | 59 ++----- 3 files changed, 115 insertions(+), 122 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 6b929fee06..2b59214938 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1155,7 +1155,7 @@ public bool HasOwnershipStatus(OwnershipStatus status) /// /// When in client-server mode, authority should is not considered the same as ownership. /// - public bool HasAuthority => IsSpawned ? m_HasAuthority : NetworkManager.IsServer; + public bool HasAuthority => IsSpawned ? m_HasAuthority : !NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer; private bool m_HasAuthority; @@ -1222,7 +1222,7 @@ public bool HasOwnershipStatus(OwnershipStatus status) /// /// Gets if the object has yet been spawned across the network /// - public bool IsSpawned { get; internal set; } + public bool IsSpawned { get; private set; } /// /// Gets if the object is a SceneObject. @@ -2052,70 +2052,54 @@ public void Despawn(bool destroy = true) NetworkManagerOwner.SpawnManager.DespawnObject(this, destroy); } - internal void SetupOnSpawn(ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + internal void SetupOnSpawn(ulong networkId, bool isPlayerObject, ulong ownerClientId, bool destroyWithScene) { -#pragma warning disable CS0618 // Type or member is obsolete - // Obsolete with warning means we need the underlying behaviour to keep existing - // TODO: remove in the 3.x branch - SetSceneObjectStatus(sceneObject); -#pragma warning restore CS0618 // Type or member is obsolete - - // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects - // Note: Always check SceneOriginHandle directly at this specific location. - if (InScenePlaced && SceneOriginHandle.IsEmpty()) - { - SceneOrigin = gameObject.scene; - } - NetworkObjectId = networkId; - - DestroyWithScene = sceneObject || destroyWithScene; - - IsPlayerObject = playerObject; - + IsPlayerObject = isPlayerObject; OwnerClientId = ownerClientId; - + m_IsOwner = ownerClientId == NetworkManagerOwner.LocalClientId; // When spawned, previous owner is always the first assigned owner PreviousOwnerId = ownerClientId; + m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + IsSpawned = true; - // If this is the player and the client is the owner, then lock ownership by default - if (NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.LocalClientId == ownerClientId && playerObject) + // If this is the player, and the client is the owner, then lock ownership by default + if (NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.LocalClientId == ownerClientId && isPlayerObject) { AddOwnershipExtended(OwnershipStatusExtended.Locked); } - m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; - IsSpawned = true; + if (IsSpawnAuthority) + { + SetupObservers(); + } - // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, - // then add all connected clients as observers - if (!NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.IsServer && SpawnWithObservers) + /* + * Setup scene related settings + */ + DestroyWithScene = InScenePlaced || destroyWithScene; + if (InScenePlaced) { - // If running as a server only, then make sure to always add the server's client identifier - if (!NetworkManagerOwner.IsHost) + // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects + // Note: Always check SceneOriginHandle directly at this specific location. + if (SceneOriginHandle.IsEmpty()) { - AddObserver(NetworkManagerOwner.LocalClientId); + SceneOrigin = gameObject.scene; } - // Add client observers - for (int i = 0; i < NetworkManagerOwner.ConnectedClientsIds.Count; i++) + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (InScenePlacedSourceGlobalObjectIdHash != 0) { - // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. - if (CheckObjectVisibility != null && !CheckObjectVisibility(NetworkManagerOwner.ConnectedClientsIds[i])) - { - continue; - } - AddObserver(NetworkManagerOwner.ConnectedClientsIds[i]); + PrefabGlobalObjectIdHash = InScenePlacedSourceGlobalObjectIdHash; } } - - // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set - // then assign this to the PrefabGlobalObjectIdHash - if (InScenePlaced && InScenePlacedSourceGlobalObjectIdHash != 0) + else if (ActiveSceneSynchronization) { - PrefabGlobalObjectIdHash = InScenePlacedSourceGlobalObjectIdHash; + // Just in case it is a recycled NetworkObject, unsubscribe first + SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; + SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged; } - } internal void ResetOnDespawn() @@ -2130,6 +2114,62 @@ internal void ResetOnDespawn() RemoveOwnershipExtended(OwnershipStatusExtended.Locked | OwnershipStatusExtended.Requested); } + internal void SetupObservers() + { + NetworkLog.InternalAssert(IsSpawnAuthority, "This function should only be called on the authority."); + + if (!SpawnWithObservers) + { + if (NetworkManagerOwner.DistributedAuthorityMode) + { + // Always add the owner/authority in DA mode even if SpawnWithObservers is false + // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) + AddObserver(OwnerClientId); + } + + return; + } + + // If running as a server only, then make sure to always add the server's client identifier + if (NetworkManagerOwner.IsServer && !NetworkManagerOwner.IsHost) + { + AddObserver(NetworkManager.ServerClientId); + } + + // If SpawnWithObservers is set, + // then add all connected clients as observers + foreach (var clientId in NetworkManagerOwner.ConnectedClientsIds) + { + // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. + if (CheckObjectVisibility != null && !CheckObjectVisibility(clientId)) + { + continue; + } + AddObserver(clientId); + } + + // Intentionally checking as opposed to just assigning in order to generate notification. + if (!Observers.Contains(OwnerClientId)) + { + // The owner only needs to always be included in DA mode. + if (NetworkManagerOwner.DistributedAuthorityMode) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Client-{OwnerClientId} is the owner of {name} but is not an observer! Adding owner as an observer!"); + } + AddObserver(OwnerClientId); + } + else + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"Client-{OwnerClientId} is the owner of {name} but is not an observer! This may cause issues"); + } + } + } + } + /// /// Removes all ownership of an object from any client. Can only be called from server /// @@ -2169,14 +2209,7 @@ public void ChangeOwnership(ulong newOwnerClientId) /// internal void InvokeBehaviourOnOwnershipChanged(ulong originalOwnerClientId, ulong newOwnerClientId) { - if (!IsSpawned) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"[{name}][Attempted behavior invoke on ownership changed before {nameof(NetworkObject)} was spawned]"); - } - return; - } + NetworkLog.InternalAssert(IsSpawned, "[{name}][Attempted behavior invoke on ownership changed before {nameof(NetworkObject)} was spawned]"); var distributedAuthorityMode = NetworkManagerOwner.DistributedAuthorityMode; var isServer = NetworkManagerOwner.IsServer; @@ -2729,8 +2762,6 @@ internal void InvokeBehaviourNetworkPreSpawn() internal void InvokeBehaviourNetworkSpawn() { - NetworkManagerOwner.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); - // Always invoke all InternalOnNetworkSpawn methods on each child NetworkBehaviour // ** before ** invoking OnNetworkSpawn. // This assures all NetworkVariables and RPC related tables have been initialized @@ -3531,29 +3562,13 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, return networkObject; } - /// - /// Subscribes to changes in the currently active scene - /// - /// - /// Only for dynamically spawned NetworkObjects - /// - internal void SubscribeToActiveSceneForSynch() - { - if (ActiveSceneSynchronization) - { - if (!InScenePlaced) - { - // Just in case it is a recycled NetworkObject, unsubscribe first - SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; - SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged; - } - } - } - /// /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating /// a NetworkObject's scene information. /// + /// + /// Should only be used for dynamically spawned NetworkObjects + /// private void CurrentlyActiveSceneChanged(Scene current, Scene next) { // Early exit if the NetworkObject is not spawned, is an in-scene placed NetworkObject, diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index 12fd9a60bb..eead80100c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Unity.Netcode.Logging; using UnityEngine; +using UnityEngine.Assertions; namespace Unity.Netcode { @@ -158,5 +160,16 @@ private static bool TryGetNetworkObjectName([NotNull] NetworkManager networkMana return true; } + [Conditional("NETCODE_INTERNAL")] + internal static void InternalAssert(bool condition, string message) + { + Assert.IsTrue(condition, message); + } + + [Conditional("NETCODE_CHECK_OWNERSHIP")] + internal static void OtherAssert(bool condition, string message) + { + Assert.IsTrue(condition, message); + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 95f1b3d305..c7404a2637 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1137,48 +1137,13 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n networkObject.NetworkManagerOwner = NetworkManager; networkObject.InvokeBehaviourNetworkPreSpawn(); - // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning - // For now, this is the best place I could find to add all connected clients as observers for newly - // instantiated and spawned NetworkObjects on the authoritative side. - if (NetworkManager.DistributedAuthorityMode) + if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) { - if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) - { - networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; - networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; - } - - // Always add the owner/authority even if SpawnWithObservers is false - // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) - if (!networkObject.SpawnWithObservers) - { - networkObject.AddObserver(ownerClientId); - } - else - { - foreach (var clientId in NetworkManager.ConnectionManager.ConnectedClientIds) - { - // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) - { - continue; - } - networkObject.AddObserver(clientId); - } - - // Sanity check to make sure the owner is always included - // Intentionally checking as opposed to just assigning in order to generate notification. - if (!networkObject.Observers.Contains(ownerClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); - } - networkObject.AddObserver(ownerClientId); - } - } + networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; + networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; } + if (!SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene)) { if (NetworkManager.LogLevel <= LogLevel.Error) @@ -1321,7 +1286,13 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong return false; } - networkObject.SetupOnSpawn(networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); +#pragma warning disable CS0618 // Type or member is obsolete + // Obsolete with warning means we need the underlying behaviour to keep existing + // TODO: remove in the 3.x branch + networkObject.SetSceneObjectStatus(sceneObject); +#pragma warning restore CS0618 // Type or member is obsolete + + networkObject.SetupOnSpawn(networkId, playerObject, ownerClientId, destroyWithScene); SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); SpawnedObjectsList.Add(networkObject); @@ -1330,15 +1301,9 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong NetworkObject.CheckOrphanChildren(); AddNetworkObjectToSceneChangedUpdates(networkObject); - + UpdateOwnershipTable(networkObject, ownerClientId); networkObject.InvokeBehaviourNetworkSpawn(); - // Only dynamically spawned NetworkObjects are allowed - if (!sceneObject) - { - networkObject.SubscribeToActiveSceneForSynch(); - } - if (networkObject.IsPlayerObject) { UpdateNetworkClientPlayer(networkObject); From 3c122f83847271125b3e44ec4abb607e51bb88ac Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 23 Jun 2026 16:08:22 -0400 Subject: [PATCH 9/9] Fix the fix --- .../Runtime/Core/NetworkObject.cs | 15 ++-------- .../Runtime/Logging/NetworkLog.cs | 7 +---- .../Runtime/Spawning/NetworkSpawnManager.cs | 3 +- .../NetworkObjectOnSpawnTests.cs | 28 ++++++++----------- 4 files changed, 16 insertions(+), 37 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 2b59214938..4649761a0b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1443,10 +1443,7 @@ public bool IsNetworkVisibleTo(ulong clientId) /// internal Scene SceneOrigin { - get - { - return m_SceneOrigin; - } + get => m_SceneOrigin; set { @@ -1466,13 +1463,8 @@ internal Scene SceneOrigin /// internal NetworkSceneHandle GetSceneOriginHandle() { - if (SceneOriginHandle.IsEmpty() && IsSpawned && InScenePlaced) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"{nameof(GetSceneOriginHandle)} called when {nameof(SceneOriginHandle)} is still zero but the {nameof(NetworkObject)} is already spawned!"); - } - } + NetworkLog.InternalAssert(!(IsSpawned && InScenePlaced && SceneOriginHandle.IsEmpty()), $"Spawned in scene placed NetworkObject {name} should always have a valid SceneOriginHandle"); + return !SceneOriginHandle.IsEmpty() ? SceneOriginHandle : gameObject.scene.handle; } @@ -2057,7 +2049,6 @@ internal void SetupOnSpawn(ulong networkId, bool isPlayerObject, ulong ownerClie NetworkObjectId = networkId; IsPlayerObject = isPlayerObject; OwnerClientId = ownerClientId; - m_IsOwner = ownerClientId == NetworkManagerOwner.LocalClientId; // When spawned, previous owner is always the first assigned owner PreviousOwnerId = ownerClientId; m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index eead80100c..df08bee61f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -160,16 +160,11 @@ private static bool TryGetNetworkObjectName([NotNull] NetworkManager networkMana return true; } + [HideInCallstack] [Conditional("NETCODE_INTERNAL")] internal static void InternalAssert(bool condition, string message) { Assert.IsTrue(condition, message); } - - [Conditional("NETCODE_CHECK_OWNERSHIP")] - internal static void OtherAssert(bool condition, string message) - { - Assert.IsTrue(condition, message); - } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index c7404a2637..3a6ada922c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1461,8 +1461,7 @@ internal void ServerResetShutdownStateForSceneObjects() { continue; } - sobj.IsSpawned = false; - sobj.DestroyWithScene = false; + sobj.ResetOnDespawn(); } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs index 0cdb31c11e..5b5583f31d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs @@ -341,7 +341,17 @@ bool HasConditionBeenMet() Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns!"); //----------- step 2 check spawn and destroy again - authorityInstance.GetComponent().Spawn(); + var authorityObject = authorityInstance.GetComponent(); + authorityObject.Spawn(); + + // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject + // when the scene of origin hasn't been set. + var sceneOriginHandle = authorityObject.GetSceneOriginHandle(); + + // This validates that GetSceneOriginHandle will return the GameObject's scene handle that should be the currently active scene + var activeSceneHandle = SceneManager.GetActiveScene().handle; + Assert.IsTrue(sceneOriginHandle == activeSceneHandle, $"{nameof(NetworkObject)} should have returned the active scene handle of {activeSceneHandle} but returned {sceneOriginHandle}"); + // wait a tick yield return s_DefaultWaitForTick; // check spawned again on server this is 2 because we are reusing the object which was already spawned once. @@ -366,22 +376,6 @@ bool HasConditionBeenMet() Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns! (2nd pass)"); } - [Test] - public void DynamicallySpawnedNoSceneOriginException() - { - var gameObject = new GameObject(); - var networkObject = gameObject.AddComponent(); - networkObject.IsSpawned = true; - networkObject.SceneOriginHandle = default; - // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject - // when the scene of origin hasn't been set. - var sceneOriginHandle = networkObject.GetSceneOriginHandle(); - - // This validates that GetSceneOriginHandle will return the GameObject's scene handle that should be the currently active scene - var activeSceneHandle = SceneManager.GetActiveScene().handle; - Assert.IsTrue(sceneOriginHandle == activeSceneHandle, $"{nameof(NetworkObject)} should have returned the active scene handle of {activeSceneHandle} but returned {sceneOriginHandle}"); - } - private class TrackOnSpawnFunctions : NetworkBehaviour { public int OnNetworkSpawnCalledCount { get; private set; }