Skip to content
36 changes: 28 additions & 8 deletions Assets/Mirror/Core/NetworkBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,9 @@ public void GeneratedSyncVarSetter<T>(T value, ref T field, ulong dirtyBit, Acti
// in client-only mode, OnDeserialize would call it.
// we use hook guard to protect against deadlock where hook
// changes syncvar, calling hook again.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
// IMPORTANT: only call hook if object is visible to host client (in NetworkClient.spawned).
// This prevents hooks from firing at spawn for objects out of AOI range.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit) && NetworkClient.spawned.ContainsKey(netIdentity.netId))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
Expand All @@ -592,7 +594,9 @@ public void GeneratedSyncVarSetter_GameObject(GameObject value, ref GameObject f
// in client-only mode, OnDeserialize would call it.
// we use hook guard to protect against deadlock where hook
// changes syncvar, calling hook again.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
// IMPORTANT: only call hook if object is visible to host client (in NetworkClient.spawned).
// This prevents hooks from firing at spawn for objects out of AOI range.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit) && NetworkClient.spawned.ContainsKey(netIdentity.netId))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
Expand All @@ -619,7 +623,9 @@ public void GeneratedSyncVarSetter_NetworkIdentity(NetworkIdentity value, ref Ne
// in client-only mode, OnDeserialize would call it.
// we use hook guard to protect against deadlock where hook
// changes syncvar, calling hook again.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
// IMPORTANT: only call hook if object is visible to host client (in NetworkClient.spawned).
// This prevents hooks from firing at spawn for objects out of AOI range.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit) && NetworkClient.spawned.ContainsKey(netIdentity.netId))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
Expand Down Expand Up @@ -647,7 +653,9 @@ public void GeneratedSyncVarSetter_NetworkBehaviour<T>(T value, ref T field, ulo
// in client-only mode, OnDeserialize would call it.
// we use hook guard to protect against deadlock where hook
// changes syncvar, calling hook again.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
// IMPORTANT: only call hook if object is visible to host client (in NetworkClient.spawned).
// This prevents hooks from firing at spawn for objects out of AOI range.
if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit) && NetworkClient.spawned.ContainsKey(netIdentity.netId))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
Expand Down Expand Up @@ -795,7 +803,10 @@ public void GeneratedSyncVarDeserialize<T>(ref T field, Action<T, T> OnChanged,
field = value;

// any hook? then call if changed.
if (OnChanged != null && !SyncVarEqual(previous, ref field))
// in host mode initial spawn, also call hook even if value hasn't changed,
// because the field was already set on server but hook wasn't called yet.
bool changed = !SyncVarEqual(previous, ref field);
if (OnChanged != null && (changed || netIdentity.hostInitialSpawn))
{
OnChanged(previous, field);
}
Expand Down Expand Up @@ -856,7 +867,10 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action<
field = GetSyncVarGameObject(netIdField, ref field);

// any hook? then call if changed.
if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField))
// in host mode initial spawn, also call hook even if value hasn't changed,
// because the field was already set on server but hook wasn't called yet.
bool changed = !SyncVarEqual(previousNetId, ref netIdField);
if (OnChanged != null && (changed || netIdentity.hostInitialSpawn))
{
OnChanged(previousGameObject, field);
}
Expand Down Expand Up @@ -918,7 +932,10 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel
field = GetSyncVarNetworkIdentity(netIdField, ref field);

// any hook? then call if changed.
if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField))
// in host mode initial spawn, also call hook even if value hasn't changed,
// because the field was already set on server but hook wasn't called yet.
bool changed = !SyncVarEqual(previousNetId, ref netIdField);
if (OnChanged != null && (changed || netIdentity.hostInitialSpawn))
{
OnChanged(previousIdentity, field);
}
Expand Down Expand Up @@ -982,7 +999,10 @@ public void GeneratedSyncVarDeserialize_NetworkBehaviour<T>(ref T field, Action<
field = GetSyncVarNetworkBehaviour(netIdField, ref field);

// any hook? then call if changed.
if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField))
// in host mode initial spawn, also call hook even if value hasn't changed,
// because the field was already set on server but hook wasn't called yet.
bool changed = !SyncVarEqual(previousNetId, ref netIdField);
if (OnChanged != null && (changed || netIdentity.hostInitialSpawn))
{
OnChanged(previousBehaviour, field);
}
Expand Down
25 changes: 24 additions & 1 deletion Assets/Mirror/Core/NetworkClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,30 @@ internal static void OnHostClientSpawn(SpawnMessage message)
aoi.SetHostVisibility(identity, true);

identity.isOwned = message.isOwner;
BootstrapIdentity(identity);

// Set flag to indicate initial spawn deserialization in host mode.
// This forces SyncVar hooks to fire even if values haven't changed,
// because the field was already set on server but hook wasn't called yet.
identity.hostInitialSpawn = true;

// Configure flags before deserializing
InitializeIdentityFlags(identity);

// Deserialize components if any payload.
// This will trigger SyncVar hooks via GeneratedSyncVarDeserialize.
if (message.payload.Count > 0)
{
using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload))
{
identity.DeserializeClient(payloadReader, true);
}
}

// Clear flag after deserialization
identity.hostInitialSpawn = false;

// Invoke callbacks after deserializing
InvokeIdentityCallbacks(identity);
}
}

Expand Down
5 changes: 5 additions & 0 deletions Assets/Mirror/Core/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public sealed class NetworkIdentity : MonoBehaviour
// internal so NetworkManager can reset it from StopClient.
internal bool clientStarted;

// flag to indicate we're deserializing initial spawn in host mode.
// used to force SyncVar hooks to fire even if value hasn't changed.
// only set temporarily during OnHostClientSpawn deserialization.
internal bool hostInitialSpawn;

/// <summary>The set of network connections (players) that can see this object.</summary>
public readonly Dictionary<int, NetworkConnectionToClient> observers =
new Dictionary<int, NetworkConnectionToClient>();
Expand Down