Skip to content
Open
202 changes: 142 additions & 60 deletions Assets/Mirror/Core/NetworkBehaviour.cs

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions Assets/Mirror/Core/NetworkClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1374,12 +1374,13 @@ internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _)
// host mode callbacks /////////////////////////////////////////////////
static void OnHostClientObjectHide(ObjectHideMessage message)
{
//Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) &&
identity != null)
//Debug.Log($"NetworkClient::OnHostClientObjectHide netId:{message.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
if (aoi != null)
aoi.SetHostVisibility(identity, false);

spawned.Remove(message.netId);
}
}

Expand Down Expand Up @@ -1425,6 +1426,10 @@ internal static void OnHostClientSpawn(SpawnMessage message)

// Invoke callbacks after deserializing
InvokeIdentityCallbacks(identity);

// Clear stored original SyncVar values
foreach (NetworkBehaviour comp in identity.NetworkBehaviours)
comp.hostModeOriginalValues.Clear();
}
}

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 @@ -123,6 +123,10 @@ public sealed class NetworkIdentity : MonoBehaviour
// only set temporarily during OnHostClientSpawn deserialization.
internal bool hostInitialSpawn;

// flag to indicate this object's spawn messages should be deferred
// until the next frame, allowing user code to set SyncVars after NetworkServer.Spawn()
internal bool deferSpawnMessages;

/// <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 Expand Up @@ -345,6 +349,7 @@ internal void InitializeNetworkBehaviours()
NetworkBehaviour component = NetworkBehaviours[i];
component.netIdentity = this;
component.ComponentIndex = (byte)i;
component.CaptureHostModeOriginalValues();
}
}

Expand Down
33 changes: 33 additions & 0 deletions Assets/Mirror/Core/NetworkServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public static partial class NetworkServer
public static readonly Dictionary<uint, NetworkIdentity> spawned =
new Dictionary<uint, NetworkIdentity>();

// deferred spawning to allow post-spawn SyncVar modifications
// objects in this list will have their spawn messages sent in the next NetworkEarlyUpdate
static readonly HashSet<NetworkIdentity> deferredSpawns = new HashSet<NetworkIdentity>();

/// <summary>Single player mode can set listen=false to not accept incoming connections.</summary>
public static bool listen;

Expand Down Expand Up @@ -274,6 +278,7 @@ public static void Shutdown()
connections.Clear();
connectionsCopy.Clear();
handlers.Clear();
deferredSpawns.Clear();

// destroy all spawned objects, _then_ set inactive.
// make sure .active is still true before calling this.
Expand Down Expand Up @@ -1794,6 +1799,9 @@ static void SpawnObject(GameObject obj, NetworkConnectionToClient ownerConnectio
identity.OnStartServer();
}

// defer spawn messages to allow post-spawn SyncVar modifications
identity.deferSpawnMessages = true;

// Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}");

if (aoi)
Expand Down Expand Up @@ -2030,6 +2038,13 @@ internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity ide
// both worlds without any worrying now!
public static void RebuildObservers(NetworkIdentity identity, bool initialize)
{
// if spawn messages are deferred, add to deferred list and skip rebuild
if (identity.deferSpawnMessages)
{
deferredSpawns.Add(identity);
return;
}

// if there is no interest management system,
// or if 'force shown' then add all connections
if (aoi == null || identity.visibility == Visibility.ForceShown)
Expand Down Expand Up @@ -2293,6 +2308,21 @@ static void Broadcast(bool unreliableBaselineElapsed)
}
}

static void ProcessDeferredSpawns()
{
// process all deferred spawns
foreach (NetworkIdentity identity in deferredSpawns)
{
if (identity != null)
{
// clear the defer flag and rebuild observers to send spawn messages
identity.deferSpawnMessages = false;
RebuildObservers(identity, true);
}
}
deferredSpawns.Clear();
}

// update //////////////////////////////////////////////////////////////
// NetworkEarlyUpdate called before any Update/FixedUpdate
// (we add this to the UnityEngine in NetworkLoop)
Expand All @@ -2305,6 +2335,9 @@ internal static void NetworkEarlyUpdate()
fullUpdateDuration.Begin();
}

// process deferred spawns first to allow post-spawn SyncVar modifications
ProcessDeferredSpawns();

// process all incoming messages first before updating the world
if (Transport.active != null)
Transport.active.ServerEarlyUpdate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,18 @@ public bool Process(ref bool WeavingFailed)
{
// only process once
if (WasProcessed(netBehaviourSubclass))
{
return false;
}

MarkAsProcessed(netBehaviourSubclass);

// deconstruct tuple and set fields
(syncVars, syncVarNetIds, syncVarHookDelegates) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed);

syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);

// Generate CaptureHostModeOriginalValues method
if (syncVars.Count > 0)
syncVarAttributeProcessor.GenerateCaptureHostModeOriginalValues(netBehaviourSubclass, syncVars, ref WeavingFailed);

ProcessMethods(ref WeavingFailed);
if (WeavingFailed)
{
Expand Down Expand Up @@ -576,7 +577,7 @@ void GenerateSerialization(ref bool WeavingFailed)
netBehaviourSubclass.Methods.Add(serialize);
}

void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool WeavingFailed)
void DeserializeField(FieldDefinition syncVar, ILProcessor worker, int dirtyBit, ref bool WeavingFailed)
{
// put 'this.' onto stack for 'this.syncvar' below
worker.Append(worker.Create(OpCodes.Ldarg_0));
Expand Down Expand Up @@ -620,6 +621,10 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);

// NEW: push dirtyBit
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);

worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject);
}
else if (syncVar.FieldType.Is<NetworkIdentity>())
Expand All @@ -631,6 +636,10 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);

// NEW: push dirtyBit
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);

worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity);
}
// handle both NetworkBehaviour and inheritors.
Expand All @@ -645,6 +654,10 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);

// NEW: push dirtyBit
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);

// make generic version of GeneratedSyncVarSetter_NetworkBehaviour<T>
MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType);
worker.Emit(OpCodes.Call, getFunc);
Expand All @@ -667,6 +680,9 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav
// reader.Read()
worker.Emit(OpCodes.Call, readFunc);

// NEW: push dirtyBit
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);

// make generic version of GeneratedSyncVarDeserialize<T>
MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType);
worker.Emit(OpCodes.Call, generic);
Expand Down Expand Up @@ -715,9 +731,11 @@ void GenerateDeSerialization(ref bool WeavingFailed)
serWorker.Append(serWorker.Create(OpCodes.Ldarg_2));
serWorker.Append(serWorker.Create(OpCodes.Brfalse, initialStateLabel));

int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
foreach (FieldDefinition syncVar in syncVars)
{
DeserializeField(syncVar, serWorker, ref WeavingFailed);
DeserializeField(syncVar, serWorker, dirtyBit, ref WeavingFailed); // Pass dirtyBit
dirtyBit += 1;
}

serWorker.Append(serWorker.Create(OpCodes.Ret));
Expand All @@ -732,7 +750,7 @@ void GenerateDeSerialization(ref bool WeavingFailed)

// conditionally read each syncvar
// start at number of syncvars in parent
int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
foreach (FieldDefinition syncVar in syncVars)
{
Instruction varLabel = serWorker.Create(OpCodes.Nop);
Expand All @@ -743,7 +761,7 @@ void GenerateDeSerialization(ref bool WeavingFailed)
serWorker.Append(serWorker.Create(OpCodes.And));
serWorker.Append(serWorker.Create(OpCodes.Brfalse, varLabel));

DeserializeField(syncVar, serWorker, ref WeavingFailed);
DeserializeField(syncVar, serWorker, dirtyBit, ref WeavingFailed); // Pass dirtyBit

serWorker.Append(varLabel);
dirtyBit += 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,63 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina
return get;
}

// Generates the CaptureHostModeOriginalValues method that captures original SyncVar field values
// before OnStartServer runs in host mode. This fixes the issue where SyncVar hooks would fire
// with incorrect oldValue parameters (oldValue == newValue) because the server had already
// modified the fields before client deserialization occurred.
public void GenerateCaptureHostModeOriginalValues(TypeDefinition td, List<FieldDefinition> syncVars, ref bool WeavingFailed)
{
// Override the empty CaptureHostModeOriginalValues method from NetworkBehaviour base class
const string MethodName = "CaptureHostModeOriginalValues";

MethodDefinition method = new MethodDefinition(MethodName,
MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.ReuseSlot,
weaverTypes.Import(typeof(void)));

ILProcessor worker = method.Body.GetILProcessor();

// Generate early return if not in host mode: if (!NetworkServer.activeHost) return;
// Only capture values in host mode where both server and client are active
Instruction returnLabel = worker.Create(OpCodes.Ret);
worker.Emit(OpCodes.Call, weaverTypes.NetworkServerGetActive);
worker.Emit(OpCodes.Brfalse, returnLabel);
worker.Emit(OpCodes.Call, weaverTypes.NetworkClientGetActive);
worker.Emit(OpCodes.Brfalse, returnLabel);

// Call helper method to clear dictionary (no complex IL needed!)
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Call, weaverTypes.clearHostModeOriginalValuesReference);

// Generate capture code for each SyncVar
int dirtyBit = syncVarAccessLists.GetSyncVarStart(td.BaseType.FullName);
foreach (FieldDefinition syncVar in syncVars)
{
// Call helper method to store value (no complex IL needed!)
worker.Emit(OpCodes.Ldarg_0); // this
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit); // dirtyBit key

// Load field value
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, syncVar);

// Box if needed
if (syncVar.FieldType.IsValueType)
{
TypeReference boxType = assembly.MainModule.ImportReference(syncVar.FieldType);
worker.Emit(OpCodes.Box, boxType);
}

// Call helper method
worker.Emit(OpCodes.Call, weaverTypes.storeHostModeOriginalValueReference);


dirtyBit += 1;
}

worker.Append(returnLabel);
td.Methods.Add(method);
}

// for [SyncVar] health, weaver generates
//
// NetworkHealth
Expand Down
11 changes: 11 additions & 0 deletions Assets/Mirror/Editor/Weaver/WeaverTypes.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Mono.CecilX;
using Mono.CecilX.Rocks;
using UnityEditor;
using UnityEngine;

Expand Down Expand Up @@ -34,6 +36,10 @@ public class WeaverTypes
// Action<T,T> for SyncVar Hooks
public MethodReference ActionT_T;

public FieldReference hostModeOriginalValuesReference;
public MethodReference clearHostModeOriginalValuesReference;
public MethodReference storeHostModeOriginalValueReference;

// syncvar
public MethodReference generatedSyncVarSetter;
public MethodReference generatedSyncVarSetter_GameObject;
Expand Down Expand Up @@ -95,6 +101,11 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail

TypeReference NetworkBehaviourType = Import<NetworkBehaviour>();

hostModeOriginalValuesReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "hostModeOriginalValues", ref WeavingFailed);

clearHostModeOriginalValuesReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "ClearHostModeOriginalValues", ref WeavingFailed);
storeHostModeOriginalValueReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "StoreHostModeOriginalValue", ref WeavingFailed);

NetworkBehaviourIsClientReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "get_isClient", ref WeavingFailed);
NetworkBehaviourIsServerReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "get_isServer", ref WeavingFailed);

Expand Down
Loading
Loading