feat: allow types that inherit from Networkbehaviour to be used in syncvar's (#2471)

* adding MakeGeneric method

* adding tests for NB syncvar+hook

* adding functions to NetworkBehaviour

* getting references to new functions

* fixing NB read so it always reads same number of bytes as write

* adding backing field and serialize for Nb Syncvar

* extra test

* adding ignore to transform test

* fixing test
This commit is contained in:
James Frowen 2020-12-09 02:53:26 +00:00 committed by GitHub
parent df89f90437
commit c3e2a26378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 697 additions and 79 deletions

View File

@ -90,7 +90,8 @@ public static bool IsMultidimensionalArray(this TypeReference tr)
public static bool IsNetworkIdentityField(this TypeReference tr) public static bool IsNetworkIdentityField(this TypeReference tr)
{ {
return tr.Is<UnityEngine.GameObject>() return tr.Is<UnityEngine.GameObject>()
|| tr.Is<NetworkIdentity>(); || tr.Is<NetworkIdentity>()
|| tr.IsDerivedFrom<NetworkBehaviour>();
} }
public static bool CanBeResolved(this TypeReference parent) public static bool CanBeResolved(this TypeReference parent)
@ -120,6 +121,20 @@ public static bool CanBeResolved(this TypeReference parent)
return true; return true;
} }
/// <summary>
/// Makes T => Variable and imports function
/// </summary>
/// <param name="generic"></param>
/// <param name="variableReference"></param>
/// <returns></returns>
public static MethodReference MakeGeneric(this MethodReference generic, TypeReference variableReference)
{
GenericInstanceMethod instance = new GenericInstanceMethod(generic);
instance.GenericArguments.Add(variableReference);
MethodReference readFunc = Weaver.CurrentAssembly.MainModule.ImportReference(instance);
return readFunc;
}
/// <summary> /// <summary>
/// Given a method of a generic class such as ArraySegment`T.get_Count, /// Given a method of a generic class such as ArraySegment`T.get_Count,

View File

@ -500,7 +500,11 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, MethodDefinit
// check for Hook function // check for Hook function
MethodDefinition hookMethod = SyncVarProcessor.GetHookMethod(netBehaviourSubclass, syncVar); MethodDefinition hookMethod = SyncVarProcessor.GetHookMethod(netBehaviourSubclass, syncVar);
if (syncVar.FieldType.IsNetworkIdentityField()) if (syncVar.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
DeserializeNetworkBehaviourField(syncVar, worker, deserialize, hookMethod);
}
else if (syncVar.FieldType.IsNetworkIdentityField())
{ {
DeserializeNetworkIdentityField(syncVar, worker, deserialize, hookMethod); DeserializeNetworkIdentityField(syncVar, worker, deserialize, hookMethod);
} }
@ -617,6 +621,115 @@ void DeserializeNetworkIdentityField(FieldDefinition syncVar, ILProcessor worker
} }
} }
/// <summary>
/// [SyncVar] NetworkBehaviour
/// </summary>
/// <param name="syncVar"></param>
/// <param name="worker"></param>
/// <param name="deserialize"></param>
/// <param name="initialState"></param>
/// <param name="hookResult"></param>
void DeserializeNetworkBehaviourField(FieldDefinition syncVar, ILProcessor worker, MethodDefinition deserialize, MethodDefinition hookMethod)
{
/*
Generates code like:
uint oldNetId = ___qNetId.netId;
byte oldCompIndex = ___qNetId.componentIndex;
T oldSyncVar = syncvar.getter;
___qNetId.netId = reader.ReadPackedUInt32();
___qNetId.componentIndex = reader.ReadByte();
if (!SyncVarEqual(oldNetId, ref ___goNetId))
{
// getter returns GetSyncVarGameObject(___qNetId)
OnSetQ(oldSyncVar, syncvar.getter);
}
*/
// GameObject/NetworkIdentity SyncVar:
// OnSerialize sends writer.Write(go);
// OnDeserialize reads to __netId manually so we can use
// lookups in the getter (so it still works if objects
// move in and out of range repeatedly)
FieldDefinition netIdField = syncVarNetIds[syncVar];
// uint oldNetId = ___qNetId;
VariableDefinition oldNetId = new VariableDefinition(WeaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>());
deserialize.Body.Variables.Add(oldNetId);
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldfld, netIdField));
worker.Append(worker.Create(OpCodes.Stloc, oldNetId));
// GameObject/NetworkIdentity oldSyncVar = syncvar.getter;
VariableDefinition oldSyncVar = new VariableDefinition(syncVar.FieldType);
deserialize.Body.Variables.Add(oldSyncVar);
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldfld, syncVar));
worker.Append(worker.Create(OpCodes.Stloc, oldSyncVar));
// read id and store in netId field BEFORE calling the hook
// -> this makes way more sense. by definition, the hook is
// supposed to be called after it was changed. not before.
// -> setting it BEFORE calling the hook fixes the following bug:
// https://github.com/vis2k/Mirror/issues/1151 in host mode
// where the value during the Hook call would call Cmds on
// the host server, and they would all happen and compare
// values BEFORE the hook even returned and hence BEFORE the
// actual value was even set.
// put 'this.' onto stack for 'this.netId' below
worker.Append(worker.Create(OpCodes.Ldarg_0));
// reader. for 'reader.Read()' below
worker.Append(worker.Create(OpCodes.Ldarg_1));
// Read()
worker.Append(worker.Create(OpCodes.Call, Readers.GetReadFunc(WeaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>())));
// netId
worker.Append(worker.Create(OpCodes.Stfld, netIdField));
if (hookMethod != null)
{
// call Hook(this.GetSyncVarGameObject/NetworkIdentity(reader.ReadPackedUInt32()))
// because we send/receive the netID, not the GameObject/NetworkIdentity
// but only if SyncVar changed. otherwise a client would
// get hook calls for all initial values, even if they
// didn't change from the default values on the client.
// see also: https://github.com/vis2k/Mirror/issues/1278
// IMPORTANT: for GameObjects/NetworkIdentities we usually
// use SyncVarGameObjectEqual to compare equality.
// in this case however, we can just use
// SyncVarEqual with the two uint netIds.
// => this is easier weaver code because we don't
// have to get the GameObject/NetworkIdentity
// from the uint netId
// => this is faster because we void one
// GetComponent call for GameObjects to get
// their NetworkIdentity when comparing.
// Generates: if (!SyncVarEqual);
Instruction syncVarEqualLabel = worker.Create(OpCodes.Nop);
// 'this.' for 'this.SyncVarEqual'
worker.Append(worker.Create(OpCodes.Ldarg_0));
// 'oldNetId'
worker.Append(worker.Create(OpCodes.Ldloc, oldNetId));
// 'ref this.__netId'
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldflda, netIdField));
// call the function
GenericInstanceMethod syncVarEqualGm = new GenericInstanceMethod(WeaverTypes.syncVarEqualReference);
syncVarEqualGm.GenericArguments.Add(netIdField.FieldType);
worker.Append(worker.Create(OpCodes.Call, syncVarEqualGm));
worker.Append(worker.Create(OpCodes.Brtrue, syncVarEqualLabel));
// call the hook
// Generates: OnValueChanged(oldValue, this.syncVar);
SyncVarProcessor.WriteCallHookMethodUsingField(worker, hookMethod, oldSyncVar, syncVar);
// Generates: end if (!SyncVarEqual);
worker.Append(syncVarEqualLabel);
}
}
/// <summary> /// <summary>
/// [SyncVar] int/float/struct/etc.? /// [SyncVar] int/float/struct/etc.?
/// </summary> /// </summary>

View File

@ -107,6 +107,19 @@ public static MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string
worker.Append(worker.Create(OpCodes.Call, WeaverTypes.getSyncVarNetworkIdentityReference)); worker.Append(worker.Create(OpCodes.Call, WeaverTypes.getSyncVarNetworkIdentityReference));
worker.Append(worker.Create(OpCodes.Ret)); worker.Append(worker.Create(OpCodes.Ret));
} }
else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
// return this.GetSyncVarNetworkBehaviour<T>(ref field, uint netId);
// this.
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldfld, netFieldId));
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldflda, fd));
MethodReference getFunc = WeaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(fd.FieldType);
worker.Append(worker.Create(OpCodes.Call, getFunc));
worker.Append(worker.Create(OpCodes.Ret));
}
// [SyncVar] int, string, etc. // [SyncVar] int, string, etc.
else else
{ {
@ -157,6 +170,15 @@ public static MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDef
worker.Append(worker.Create(OpCodes.Call, WeaverTypes.syncVarNetworkIdentityEqualReference)); worker.Append(worker.Create(OpCodes.Call, WeaverTypes.syncVarNetworkIdentityEqualReference));
} }
else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
// reference to netId Field to set
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldfld, netFieldId));
MethodReference getFunc = WeaverTypes.syncVarNetworkBehaviourEqualReference.MakeGeneric(fd.FieldType);
worker.Append(worker.Create(OpCodes.Call, getFunc));
}
else else
{ {
worker.Append(worker.Create(OpCodes.Ldarg_0)); worker.Append(worker.Create(OpCodes.Ldarg_0));
@ -207,6 +229,15 @@ public static MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDef
worker.Append(worker.Create(OpCodes.Call, WeaverTypes.setSyncVarNetworkIdentityReference)); worker.Append(worker.Create(OpCodes.Call, WeaverTypes.setSyncVarNetworkIdentityReference));
} }
else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
// reference to netId Field to set
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Ldflda, netFieldId));
MethodReference getFunc = WeaverTypes.setSyncVarNetworkBehaviourReference.MakeGeneric(fd.FieldType);
worker.Append(worker.Create(OpCodes.Call, getFunc));
}
else else
{ {
// make generic version of SetSyncVar with field type // make generic version of SetSyncVar with field type
@ -266,7 +297,16 @@ public static void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Diction
// GameObject/NetworkIdentity SyncVars have a new field for netId // GameObject/NetworkIdentity SyncVars have a new field for netId
FieldDefinition netIdField = null; FieldDefinition netIdField = null;
if (fd.FieldType.IsNetworkIdentityField()) // NetworkBehaviour has different field type than other NetworkIdentityFields
if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
netIdField = new FieldDefinition("___" + fd.Name + "NetId",
FieldAttributes.Private,
WeaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>());
syncVarNetIds[fd] = netIdField;
}
else if (fd.FieldType.IsNetworkIdentityField())
{ {
netIdField = new FieldDefinition("___" + fd.Name + "NetId", netIdField = new FieldDefinition("___" + fd.Name + "NetId",
FieldAttributes.Private, FieldAttributes.Private,

View File

@ -142,10 +142,7 @@ private static MethodReference GetNetworkBehaviourReader(TypeReference variableR
// uses generic ReadNetworkBehaviour rather than having weaver create one for each NB // uses generic ReadNetworkBehaviour rather than having weaver create one for each NB
MethodReference generic = WeaverTypes.readNetworkBehaviourGeneric; MethodReference generic = WeaverTypes.readNetworkBehaviourGeneric;
GenericInstanceMethod instance = new GenericInstanceMethod(generic); MethodReference readFunc = generic.MakeGeneric(variableReference);
instance.GenericArguments.Add(variableReference);
MethodReference readFunc = Weaver.CurrentAssembly.MainModule.ImportReference(instance);
// register function so it is added to Reader<T> // register function so it is added to Reader<T>
// use Register instead of RegisterWriteFunc because this is not a generated function // use Register instead of RegisterWriteFunc because this is not a generated function

View File

@ -44,6 +44,9 @@ public static class WeaverTypes
public static MethodReference getSyncVarGameObjectReference; public static MethodReference getSyncVarGameObjectReference;
public static MethodReference setSyncVarNetworkIdentityReference; public static MethodReference setSyncVarNetworkIdentityReference;
public static MethodReference getSyncVarNetworkIdentityReference; public static MethodReference getSyncVarNetworkIdentityReference;
public static MethodReference syncVarNetworkBehaviourEqualReference;
public static MethodReference setSyncVarNetworkBehaviourReference;
public static MethodReference getSyncVarNetworkBehaviourReference;
public static MethodReference registerCommandDelegateReference; public static MethodReference registerCommandDelegateReference;
public static MethodReference registerRpcDelegateReference; public static MethodReference registerRpcDelegateReference;
public static MethodReference getTypeReference; public static MethodReference getTypeReference;
@ -116,6 +119,10 @@ public static void SetupTargetTypes(AssemblyDefinition currentAssembly)
getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "GetSyncVarGameObject"); getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "GetSyncVarGameObject");
setSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "SetSyncVarNetworkIdentity"); setSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "SetSyncVarNetworkIdentity");
getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "GetSyncVarNetworkIdentity"); getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "GetSyncVarNetworkIdentity");
syncVarNetworkBehaviourEqualReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "SyncVarNetworkBehaviourEqual");
setSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "SetSyncVarNetworkBehaviour");
getSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, currentAssembly, "GetSyncVarNetworkBehaviour");
registerCommandDelegateReference = Resolvers.ResolveMethod(RemoteCallHelperType, currentAssembly, "RegisterCommandDelegate"); registerCommandDelegateReference = Resolvers.ResolveMethod(RemoteCallHelperType, currentAssembly, "RegisterCommandDelegate");
registerRpcDelegateReference = Resolvers.ResolveMethod(RemoteCallHelperType, currentAssembly, "RegisterRpcDelegate"); registerRpcDelegateReference = Resolvers.ResolveMethod(RemoteCallHelperType, currentAssembly, "RegisterRpcDelegate");
TypeReference unityDebug = Import(typeof(UnityEngine.Debug)); TypeReference unityDebug = Import(typeof(UnityEngine.Debug));

View File

@ -418,6 +418,107 @@ protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdent
return identityField; return identityField;
} }
protected bool SyncVarNetworkBehaviourEqual<T>(T newBehaviour, NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour
{
uint newNetId = 0;
int newComponentIndex = 0;
if (newBehaviour != null)
{
newNetId = newBehaviour.netId;
newComponentIndex = newBehaviour.ComponentIndex;
if (newNetId == 0)
{
logger.LogWarning("SetSyncVarNetworkIdentity NetworkIdentity " + newBehaviour + " has a zero netId. Maybe it is not spawned yet?");
}
}
// netId changed?
return syncField.Equals(newNetId, newComponentIndex);
}
// helper function for [SyncVar] NetworkIdentities.
protected void SetSyncVarNetworkBehaviour<T>(T newBehaviour, ref T behaviourField, ulong dirtyBit, ref NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour
{
if (getSyncVarHookGuard(dirtyBit))
return;
uint newNetId = 0;
int componentIndex = 0;
if (newBehaviour != null)
{
newNetId = newBehaviour.netId;
componentIndex = newBehaviour.ComponentIndex;
if (newNetId == 0)
{
logger.LogWarning($"{nameof(SetSyncVarNetworkBehaviour)} NetworkIdentity " + newBehaviour + " has a zero netId. Maybe it is not spawned yet?");
}
}
// old field for log
NetworkBehaviourSyncVar oldField = syncField;
syncField = new NetworkBehaviourSyncVar(newNetId, componentIndex);
SetDirtyBit(dirtyBit);
// assign new one on the server, and in case we ever need it on client too
behaviourField = newBehaviour;
if (logger.LogEnabled()) logger.Log($"SetSyncVarNetworkBehaviour NetworkIdentity {GetType().Name} bit [{dirtyBit}] netIdField:{oldField}->{syncField}");
}
// helper function for [SyncVar] NetworkIdentities.
// -> ref GameObject as second argument makes OnDeserialize processing easier
protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour
{
// server always uses the field
if (isServer)
{
return behaviourField;
}
// client always looks up based on netId because objects might get in and out of range
// over and over again, which shouldn't null them forever
if (!NetworkIdentity.spawned.TryGetValue(syncNetBehaviour.netId, out NetworkIdentity identity))
{
return null;
}
behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T;
return behaviourField;
}
/// <summary>
/// backing field for sync NetworkBehaviour
/// </summary>
public struct NetworkBehaviourSyncVar : IEquatable<NetworkBehaviourSyncVar>
{
public uint netId;
// limtied to 255 behaviours per identity
public byte componentIndex;
public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this()
{
this.netId = netId;
this.componentIndex = (byte)componentIndex;
}
public bool Equals(NetworkBehaviourSyncVar other)
{
return other.netId == netId && other.componentIndex == componentIndex;
}
public bool Equals(uint netId, int componentIndex)
{
return this.netId == netId && this.componentIndex == componentIndex;
}
public override string ToString()
{
return $"[netId:{netId} compIndex:{componentIndex}]";
}
}
protected bool SyncVarEqual<T>(T value, ref T fieldValue) protected bool SyncVarEqual<T>(T value, ref T fieldValue)
{ {
// newly initialized or changed value? // newly initialized or changed value?

View File

@ -308,13 +308,19 @@ public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader)
public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader)
{ {
NetworkIdentity identity = reader.ReadNetworkIdentity(); uint netId = reader.ReadUInt32();
if (identity == null) if (netId == 0)
return null;
// if netId is not 0, then index is also sent to read before returning
byte componentIndex = reader.ReadByte();
if (!NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity))
{ {
if (logger.WarnEnabled()) logger.LogFormat(LogType.Warning, "ReadNetworkBehaviour netId:{0} not found in spawned", netId);
return null; return null;
} }
byte componentIndex = reader.ReadByte();
return identity.NetworkBehaviours[componentIndex]; return identity.NetworkBehaviours[componentIndex];
} }
@ -323,6 +329,20 @@ public static T ReadNetworkBehaviour<T>(this NetworkReader reader) where T : Net
return reader.ReadNetworkBehaviour() as T; return reader.ReadNetworkBehaviour() as T;
} }
public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader)
{
uint netId = reader.ReadUInt32();
byte componentIndex = default;
// if netId is not 0, then index is also sent to read before returning
if (netId != 0)
{
componentIndex = reader.ReadByte();
}
return new NetworkBehaviour.NetworkBehaviourSyncVar(netId, componentIndex);
}
public static List<T> ReadList<T>(this NetworkReader reader) public static List<T> ReadList<T>(this NetworkReader reader)
{ {
int length = reader.ReadInt32(); int length = reader.ReadInt32();

View File

@ -3,6 +3,7 @@
using Mirror.Tests.RemoteAttrributeTest; using Mirror.Tests.RemoteAttrributeTest;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools;
namespace Mirror.Tests namespace Mirror.Tests
{ {
@ -1008,6 +1009,37 @@ public void TestNullList()
Assert.That(readList, Is.Null); Assert.That(readList, Is.Null);
} }
[Test]
public void ReadNetworkIdentityGivesWarningWhenNotFound()
{
const uint netId = 423;
NetworkWriter writer = new NetworkWriter();
writer.WriteUInt32(netId);
NetworkReader reader = new NetworkReader(writer.ToArray());
LogAssert.Expect(LogType.Warning, $"ReadNetworkIdentity netId:{netId} not found in spawned");
NetworkIdentity actual = reader.ReadNetworkIdentity();
Assert.That(actual, Is.Null);
Assert.That(reader.Position, Is.EqualTo(4), "should read 4 bytes");
}
[Test]
public void ReadNetworkBehaviourGivesWarningWhenNotFound()
{
const uint netId = 424;
NetworkWriter writer = new NetworkWriter();
writer.WriteUInt32(netId);
writer.WriteByte(0);
NetworkReader reader = new NetworkReader(writer.ToArray());
LogAssert.Expect(LogType.Warning, $"ReadNetworkBehaviour netId:{netId} not found in spawned");
NetworkBehaviour actual = reader.ReadNetworkBehaviour();
Assert.That(actual, Is.Null);
Assert.That(reader.Position, Is.EqualTo(5), "should read 5 bytes when netId is not 0");
}
[Test] [Test]
public void TestNetworkBehaviour() public void TestNetworkBehaviour()
{ {
@ -1056,7 +1088,9 @@ public void TestNetworkBehaviourNull()
NetworkReader reader = new NetworkReader(bytes); NetworkReader reader = new NetworkReader(bytes);
RpcNetworkIdentityBehaviour actual = reader.ReadNetworkBehaviour<RpcNetworkIdentityBehaviour>(); RpcNetworkIdentityBehaviour actual = reader.ReadNetworkBehaviour<RpcNetworkIdentityBehaviour>();
Assert.That(actual, Is.EqualTo(null), "Read should find the same behaviour as written"); Assert.That(actual, Is.Null, "should read null");
Assert.That(reader.Position, Is.EqualTo(4), "should read 4 bytes when netid is 0");
} }
[Test] [Test]

View File

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
namespace Mirror.Tests namespace Mirror.Tests.SyncVarTests
{ {
class HookBehaviour : NetworkBehaviour class HookBehaviour : NetworkBehaviour
{ {
@ -44,6 +43,20 @@ void OnValueChanged(NetworkIdentity oldValue, NetworkIdentity newValue)
} }
} }
class NetworkBehaviourHookBehaviour : NetworkBehaviour
{
[SyncVar(hook = nameof(OnValueChanged))]
public NetworkBehaviourHookBehaviour value = null;
public event Action<NetworkBehaviourHookBehaviour, NetworkBehaviourHookBehaviour> HookCalled;
void OnValueChanged(NetworkBehaviourHookBehaviour oldValue, NetworkBehaviourHookBehaviour newValue)
{
HookCalled.Invoke(oldValue, newValue);
}
}
class StaticHookBehaviour : NetworkBehaviour class StaticHookBehaviour : NetworkBehaviour
{ {
[SyncVar(hook = nameof(OnValueChanged))] [SyncVar(hook = nameof(OnValueChanged))]
@ -99,70 +112,8 @@ protected override void OnValueChanged(int oldValue, int newValue)
} }
public class SyncVarHookTest public class SyncVarHookTest : SyncVarTestBase
{ {
private List<GameObject> spawned = new List<GameObject>();
[TearDown]
public void TearDown()
{
foreach (GameObject item in spawned)
{
GameObject.DestroyImmediate(item);
}
spawned.Clear();
NetworkIdentity.spawned.Clear();
}
T CreateObject<T>() where T : NetworkBehaviour
{
GameObject gameObject = new GameObject();
spawned.Add(gameObject);
gameObject.AddComponent<NetworkIdentity>();
T behaviour = gameObject.AddComponent<T>();
behaviour.syncInterval = 0f;
return behaviour;
}
NetworkIdentity CreateNetworkIdentity(uint netId)
{
GameObject gameObject = new GameObject();
spawned.Add(gameObject);
NetworkIdentity networkIdentity = gameObject.AddComponent<NetworkIdentity>();
networkIdentity.netId = netId;
NetworkIdentity.spawned[netId] = networkIdentity;
return networkIdentity;
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="serverObject"></param>
/// <param name="clientObject"></param>
/// <param name="initialState"></param>
/// <returns>If data was written by OnSerialize</returns>
public static bool SyncToClient<T>(T serverObject, T clientObject, bool initialState) where T : NetworkBehaviour
{
NetworkWriter writer = new NetworkWriter();
bool written = serverObject.OnSerialize(writer, initialState);
NetworkReader reader = new NetworkReader(writer.ToArray());
clientObject.OnDeserialize(reader, initialState);
int writeLength = writer.Length;
int readLength = reader.Position;
Assert.That(writeLength == readLength, $"OnSerializeAll and OnDeserializeAll calls write the same amount of data\n writeLength={writeLength}\n readLength={readLength}");
return written;
}
[Test] [Test]
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
@ -296,6 +247,33 @@ public void NetworkIdentityHook_HookCalledWhenSyncingChangedValue(bool intialSta
Assert.That(callCount, Is.EqualTo(1)); Assert.That(callCount, Is.EqualTo(1));
} }
[Test]
[TestCase(true)]
[TestCase(false)]
public void NetworkBehaviourHook_HookCalledWhenSyncingChangedValue(bool intialState)
{
NetworkBehaviourHookBehaviour serverObject = CreateObject<NetworkBehaviourHookBehaviour>();
NetworkBehaviourHookBehaviour clientObject = CreateObject<NetworkBehaviourHookBehaviour>();
NetworkBehaviourHookBehaviour clientValue = null;
NetworkBehaviourHookBehaviour serverValue = CreateNetworkIdentity(2033).gameObject.AddComponent<NetworkBehaviourHookBehaviour>();
serverObject.value = serverValue;
clientObject.value = clientValue;
int callCount = 0;
clientObject.HookCalled += (oldValue, newValue) =>
{
callCount++;
Assert.That(oldValue, Is.EqualTo(clientValue));
Assert.That(newValue, Is.EqualTo(serverValue));
};
bool written = SyncToClient(serverObject, clientObject, intialState);
Assert.IsTrue(written);
Assert.That(callCount, Is.EqualTo(1));
}
[Test] [Test]
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]

View File

@ -1,9 +1,9 @@
using System;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
namespace Mirror.Tests namespace Mirror.Tests.SyncVarTests
{ {
class MockPlayer : NetworkBehaviour class MockPlayer : NetworkBehaviour
{ {
public struct Guild public struct Guild
@ -16,9 +16,29 @@ public struct Guild
} }
public class SyncVarTest class SyncVarGameObject : NetworkBehaviour
{ {
[SyncVar]
public GameObject value;
}
class SyncVarNetworkIdentity : NetworkBehaviour
{
[SyncVar]
public NetworkIdentity value;
}
class SyncVarTransform : NetworkBehaviour
{
[SyncVar]
public Transform value;
}
class SyncVarNetworkBehaviour : NetworkBehaviour
{
[SyncVar]
public SyncVarNetworkBehaviour value;
}
public class SyncVarTest : SyncVarTestBase
{
[Test] [Test]
public void TestSettingStruct() public void TestSettingStruct()
{ {
@ -137,5 +157,206 @@ public void TestSynchronizingObjects()
// check that the syncvars got updated // check that the syncvars got updated
Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized"); Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized");
} }
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncsGameobject(bool initialState)
{
SyncVarGameObject serverObject = CreateObject<SyncVarGameObject>();
SyncVarGameObject clientObject = CreateObject<SyncVarGameObject>();
GameObject serverValue = CreateNetworkIdentity(2044).gameObject;
serverObject.value = serverValue;
clientObject.value = null;
bool written = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written);
Assert.That(clientObject.value, Is.EqualTo(serverValue));
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncIdentity(bool initialState)
{
SyncVarNetworkIdentity serverObject = CreateObject<SyncVarNetworkIdentity>();
SyncVarNetworkIdentity clientObject = CreateObject<SyncVarNetworkIdentity>();
NetworkIdentity serverValue = CreateNetworkIdentity(2045);
serverObject.value = serverValue;
clientObject.value = null;
bool written = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written);
Assert.That(clientObject.value, Is.EqualTo(serverValue));
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncTransform(bool initialState)
{
SyncVarTransform serverObject = CreateObject<SyncVarTransform>();
SyncVarTransform clientObject = CreateObject<SyncVarTransform>();
Transform serverValue = CreateNetworkIdentity(2045).transform;
serverObject.value = serverValue;
clientObject.value = null;
bool written = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written);
Assert.That(clientObject.value, Is.EqualTo(serverValue));
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncsBehaviour(bool initialState)
{
SyncVarNetworkBehaviour serverObject = CreateObject<SyncVarNetworkBehaviour>();
SyncVarNetworkBehaviour clientObject = CreateObject<SyncVarNetworkBehaviour>();
SyncVarNetworkBehaviour serverValue = CreateNetworkIdentity(2046).gameObject.AddComponent<SyncVarNetworkBehaviour>();
serverObject.value = serverValue;
clientObject.value = null;
bool written = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written);
Assert.That(clientObject.value, Is.EqualTo(serverValue));
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncsMultipleBehaviour(bool initialState)
{
SyncVarNetworkBehaviour serverObject = CreateObject<SyncVarNetworkBehaviour>();
SyncVarNetworkBehaviour clientObject = CreateObject<SyncVarNetworkBehaviour>();
NetworkIdentity identity = CreateNetworkIdentity(2046);
SyncVarNetworkBehaviour behaviour1 = identity.gameObject.AddComponent<SyncVarNetworkBehaviour>();
SyncVarNetworkBehaviour behaviour2 = identity.gameObject.AddComponent<SyncVarNetworkBehaviour>();
// create array/set indexs
_ = identity.NetworkBehaviours;
int index1 = behaviour1.ComponentIndex;
int index2 = behaviour2.ComponentIndex;
// check components of same type have different indexes
Assert.That(index1, Is.Not.EqualTo(index2));
// check behaviour 1 can be synced
serverObject.value = behaviour1;
clientObject.value = null;
bool written1 = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written1);
Assert.That(clientObject.value, Is.EqualTo(behaviour1));
// check that behaviour 2 can be synced
serverObject.value = behaviour2;
clientObject.value = null;
bool written2 = SyncToClient(serverObject, clientObject, initialState);
Assert.IsTrue(written2);
Assert.That(clientObject.value, Is.EqualTo(behaviour2));
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncVarCacheNetidForGameObject(bool initialState)
{
SyncVarCacheNetidForGeneric<SyncVarGameObject, GameObject>(
(obj) => obj.value,
(obj, value) => obj.value = value,
(identity) => identity.gameObject,
initialState
);
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncVarCacheNetidForIdentity(bool initialState)
{
SyncVarCacheNetidForGeneric<SyncVarNetworkIdentity, NetworkIdentity>(
(obj) => obj.value,
(obj, value) => obj.value = value,
(identity) => identity,
initialState
);
}
[Test]
[TestCase(true)]
[TestCase(false)]
[Ignore("Transform backing field has not been implemented yet")]
public void SyncVarCacheNetidForTransform(bool initialState)
{
SyncVarCacheNetidForGeneric<SyncVarTransform, Transform>(
(obj) => obj.value,
(obj, value) => obj.value = value,
(identity) => identity.transform,
initialState
);
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void SyncVarCacheNetidForBehaviour(bool initialState)
{
SyncVarCacheNetidForGeneric<SyncVarNetworkBehaviour, SyncVarNetworkBehaviour>(
(obj) => obj.value,
(obj, value) => obj.value = value,
(identity) => identity.gameObject.AddComponent<SyncVarNetworkBehaviour>(),
initialState
);
}
private void SyncVarCacheNetidForGeneric<TBehaviour, TValue>(
Func<TBehaviour, TValue> getField,
Action<TBehaviour, TValue> setField,
Func<NetworkIdentity, TValue> getCreatedValue,
bool initialState)
where TValue : UnityEngine.Object
where TBehaviour : NetworkBehaviour
{
TBehaviour serverObject = CreateObject<TBehaviour>();
TBehaviour clientObject = CreateObject<TBehaviour>();
NetworkIdentity identity = CreateNetworkIdentity(2047);
TValue serverValue = getCreatedValue(identity);
Assert.That(serverValue, Is.Not.Null, "getCreatedValue should not return null");
setField(serverObject, serverValue);
setField(clientObject, null);
// write server data
bool written = ServerWrite(serverObject, initialState, out ArraySegment<byte> data, out int writeLength);
Assert.IsTrue(written, "did not write");
// remove identity from collection
NetworkIdentity.spawned.Remove(identity.netId);
// read client data, this should be cached in field
ClientRead(clientObject, initialState, data, writeLength);
// check field shows as null
Assert.That(getField(clientObject), Is.EqualTo(null), "field should return null");
// add identity back to collection
NetworkIdentity.spawned.Add(identity.netId, identity);
// check field finds value
Assert.That(getField(clientObject), Is.EqualTo(serverValue), "fields should return serverValue");
}
} }
} }

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
namespace Mirror.Tests
{
public class SyncVarTestBase
{
readonly List<GameObject> spawned = new List<GameObject>();
[TearDown]
public void TearDown()
{
foreach (GameObject item in spawned)
{
GameObject.DestroyImmediate(item);
}
spawned.Clear();
NetworkIdentity.spawned.Clear();
}
protected T CreateObject<T>() where T : NetworkBehaviour
{
GameObject gameObject = new GameObject();
spawned.Add(gameObject);
gameObject.AddComponent<NetworkIdentity>();
T behaviour = gameObject.AddComponent<T>();
behaviour.syncInterval = 0f;
return behaviour;
}
protected NetworkIdentity CreateNetworkIdentity(uint netId)
{
GameObject gameObject = new GameObject();
spawned.Add(gameObject);
NetworkIdentity networkIdentity = gameObject.AddComponent<NetworkIdentity>();
networkIdentity.netId = netId;
NetworkIdentity.spawned[netId] = networkIdentity;
return networkIdentity;
}
/// <returns>If data was written by OnSerialize</returns>
public static bool SyncToClient<T>(T serverObject, T clientObject, bool initialState) where T : NetworkBehaviour
{
bool written = ServerWrite(serverObject, initialState, out ArraySegment<byte> data, out int writeLength);
ClientRead(clientObject, initialState, data, writeLength);
return written;
}
public static bool ServerWrite<T>(T serverObject, bool initialState, out ArraySegment<byte> data, out int writeLength) where T : NetworkBehaviour
{
NetworkWriter writer = new NetworkWriter();
bool written = serverObject.OnSerialize(writer, initialState);
writeLength = writer.Length;
data = writer.ToArraySegment();
return written;
}
public static void ClientRead<T>(T clientObject, bool initialState, ArraySegment<byte> data, int writeLength) where T : NetworkBehaviour
{
NetworkReader reader = new NetworkReader(data);
clientObject.OnDeserialize(reader, initialState);
int readLength = reader.Position;
Assert.That(writeLength == readLength,
$"OnSerializeAll and OnDeserializeAll calls write the same amount of data\n" +
$" writeLength={writeLength}\n" +
$" readLength={readLength}");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6fbd8fff4a0795d49a0b122554ed6b13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: